diff --git a/.gitignore b/.gitignore index 37345c3..72aa169 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,5 @@ build /docs lib /oss-attribution -*.tmp \ No newline at end of file +*.tmp +*.pdf \ No newline at end of file diff --git a/package.json b/package.json index f2e080c..eb1477f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "html-pdf": "^2.2.0", "reflect-metadata": "^0.1.13", "routing-controllers": "^0.9.0-alpha.6", "routing-controllers-openapi": "^2.2.0" @@ -72,4 +73,4 @@ "publish": false } } -} \ No newline at end of file +} diff --git a/src/PdfCreator.ts b/src/PdfCreator.ts new file mode 100644 index 0000000..2345c8c --- /dev/null +++ b/src/PdfCreator.ts @@ -0,0 +1,56 @@ +import fs from "fs"; +import pdf_converter from "html-pdf"; +import path from 'path'; +import { Stream } from 'stream'; + +/** + * This class is responsible for all things pdf creation. + * This uses the html templates from src/templates. + */ +export class PdfCreator { + private templateDir = path.join(__dirname, '/templates'); + + //TODO: Accept the runner class + public async generateSponsoringContract(): Promise { + let template = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8'); + template = template.replace("{{Runner Name}}", "lelele"); + return new Pdf(await pdf_converter.create(template, { format: "A5", orientation: "landscape" })); + } + +} + +/** + * This class is a wrapper for the pdf objects created by html-pdf. + * It offers typed conversion to Buffer and Stream. + */ +export class Pdf { + content: any; + + constructor(pdf: any) { + this.content = pdf; + } + + /** + * Promise wrapper function that resolves the toBuffer promise for pdf generation. + */ + public async toBuffer(): Promise { + let promise = await new Promise((resolve, reject) => { + this.content.toBuffer(function (err, buffer: Buffer) { + resolve(buffer); + }); + }); + return await promise; + } + + /** + * Promise wrapper function that resolves the toStream promise for pdf generation. + */ + public async toStream(): Promise { + let promise = await new Promise((resolve, reject) => { + this.content.toStream(function (err, stream: Stream) { + resolve(stream); + }); + }); + return await promise; + } +} \ No newline at end of file diff --git a/src/controllers/PdfController.ts b/src/controllers/PdfController.ts new file mode 100644 index 0000000..0ec9581 --- /dev/null +++ b/src/controllers/PdfController.ts @@ -0,0 +1,20 @@ +import { ContentType, Controller, Get } from 'routing-controllers'; +import { OpenAPI } from 'routing-controllers-openapi'; +import { PdfCreator } from '../PdfCreator'; + +@Controller() +export class PdfController { + private pdf: PdfCreator; + constructor() { + this.pdf = new PdfCreator(); + } + + @Get('/contracts') + @ContentType("application/pdf") + @OpenAPI({ description: "Generate Sponsoring contract pdfs from runner objects." }) + async generateContracts() { + //TODO: Accept the real classes + const contracts = await this.pdf.generateSponsoringContract(); + return await contracts.toBuffer(); + } +} \ No newline at end of file diff --git a/src/errors/AddressErrors.ts b/src/errors/AddressErrors.ts new file mode 100644 index 0000000..300bb31 --- /dev/null +++ b/src/errors/AddressErrors.ts @@ -0,0 +1,57 @@ +import { IsString } from 'class-validator'; +import { BadRequestError } from 'routing-controllers'; + +/** + * Error to throw when an address's postal code fails validation. + */ +export class AddressPostalCodeInvalidError extends BadRequestError { + @IsString() + name = "AddressPostalCodeInvalidError" + + @IsString() + message = "The postal code you provided is invalid. \n Please check if your postal code follows the postal code validation guidelines." +} + +/** + * Error to throw when an non-empty address's first line isn't set. + */ +export class AddressFirstLineEmptyError extends BadRequestError { + @IsString() + name = "AddressFirstLineEmptyError" + + @IsString() + message = "You provided a empty first address line. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country" +} + +/** + * Error to throw when an non-empty address's postal code isn't set. + */ +export class AddressPostalCodeEmptyError extends BadRequestError { + @IsString() + name = "AddressPostalCodeEmptyError" + + @IsString() + message = "You provided a empty postal code. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country" +} + +/** + * Error to throw when an non-empty address's city isn't set. + */ +export class AddressCityEmptyError extends BadRequestError { + @IsString() + name = "AddressCityEmptyError" + + @IsString() + message = "You provided a empty city. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country" +} + +/** + * Error to throw when an non-empty address's country isn't set. + */ +export class AddressCountryEmptyError extends BadRequestError { + @IsString() + name = "AddressCountryEmptyError" + + @IsString() + message = "You provided a empty country. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country" +} \ No newline at end of file diff --git a/src/models/Address.ts b/src/models/Address.ts new file mode 100644 index 0000000..dc66f10 --- /dev/null +++ b/src/models/Address.ts @@ -0,0 +1,80 @@ +import { + IsPostalCode, + IsString +} from "class-validator"; +import ValidatorJS from 'validator'; +import { config } from '../../config'; +import { AddressCityEmptyError, AddressCountryEmptyError, AddressFirstLineEmptyError, AddressPostalCodeEmptyError, AddressPostalCodeInvalidError } from '../../errors/AddressErrors'; + +/** + * Defines the Address class. + * Implemented this way to prevent any formatting differences. +*/ +export class Address { + /** + * The address's first line. + * Containing the street and house number. + */ + @IsString() + address1?: string; + + /** + * The address's second line. + * Containing optional information. + */ + @IsString() + address2?: string; + + /** + * The address's postal code. + * This will get checked against the postal code syntax for the configured country. + */ + @IsString() + @IsPostalCode(config.postalcode_validation_countrycode) + postalcode: string; + + /** + * The address's city. + */ + @IsString() + city: string; + + /** + * The address's country. + */ + @IsString() + country: string; + + public reset() { + this.address1 = null; + this.address2 = null; + this.city = null; + this.country = null; + this.postalcode = null; + } + + /** + * Checks if this is a valid address + */ + public static isValidAddress(address: Address): Boolean { + if (address == null) { return false; } + if (address.address1 == null || address.city == null || address.country == null || address.postalcode == null) { return false; } + if (ValidatorJS.isPostalCode(address.postalcode, config.postalcode_validation_countrycode) == false) { return false; } + return true; + } + + /** + * This function validates addresses. + * This is a workaround for non-existant class validation for embedded entities. + * @param address The address that shall get validated. + */ + public static validate(address: Address) { + if (address == null) { return; } + if (address.address1 == null && address.city == null && address.country == null && address.postalcode == null) { return; } + if (address.address1 == null) { throw new AddressFirstLineEmptyError(); } + if (address.postalcode == null) { throw new AddressPostalCodeEmptyError(); } + if (address.city == null) { throw new AddressCityEmptyError(); } + if (address.country == null) { throw new AddressCountryEmptyError(); } + if (ValidatorJS.isPostalCode(address.postalcode.toString(), config.postalcode_validation_countrycode) == false) { throw new AddressPostalCodeInvalidError(); } + } +} diff --git a/src/models/CertificateRunner.ts b/src/models/CertificateRunner.ts new file mode 100644 index 0000000..a30b356 --- /dev/null +++ b/src/models/CertificateRunner.ts @@ -0,0 +1,16 @@ +import { + IsArray +} from "class-validator"; +import { DistanceDonation } from './DistanceDonation'; +import { Runner } from './Runner'; + +/** + * Defines the certificate runner class (from which the runner certificates get generated). +*/ +export class CertificateRunner extends Runner { + /** + * Array containing all distance donations associated with the runner. + */ + @IsArray() + distanceDonations: DistanceDonation[]; +} diff --git a/src/models/DistanceDonation.ts b/src/models/DistanceDonation.ts new file mode 100644 index 0000000..a4e35e1 --- /dev/null +++ b/src/models/DistanceDonation.ts @@ -0,0 +1,40 @@ +import { IsInt, IsNotEmpty, IsObject, IsPositive } from "class-validator"; +import { Donation } from "./Donation"; +import { Runner } from "./Runner"; + +/** + * Defines the DistanceDonation class. + * For distanceDonations a donor pledges to donate a certain amount for each kilometer ran by a runner. +*/ +export class DistanceDonation extends Donation { + /** + * The donation's associated runner. + * Used as the source of the donation's distance. + */ + @IsObject() + @IsNotEmpty() + runner: Runner; + + /** + * The donation's amount donated per distance. + * The amount the donor set to be donated per kilometer that the runner ran. + */ + @IsInt() + @IsPositive() + amountPerDistance: number; + + /** + * The donation's amount in cents (or whatever your currency's smallest unit is.). + * Get's calculated from the runner's distance ran and the amount donated per kilometer. + */ + public get amount(): number { + let calculatedAmount = 0; + try { + calculatedAmount = this.amountPerDistance * (this.runner.distance / 1000); + } catch (error) { + throw error; + } + return calculatedAmount; + } + +} diff --git a/src/models/Donation.ts b/src/models/Donation.ts new file mode 100644 index 0000000..1beaf0c --- /dev/null +++ b/src/models/Donation.ts @@ -0,0 +1,32 @@ +import { + IsInt, + IsNotEmpty, + IsObject +} from "class-validator"; +import { Donor } from './Donor'; + +/** + * Defines the Donation base calss. + * A donation just associates a donor with a donation amount. + * The specifics of the amoun's determination has to be implemented in child classes. +*/ +export abstract class Donation { + /** + * Autogenerated unique id (primary key). + */ + @IsInt() + id: number; + + /** + * The donations's donor. + */ + @IsNotEmpty() + @IsObject() + donor: Donor; + + /** + * The donation's amount in cents (or whatever your currency's smallest unit is.). + * The exact implementation may differ for each type of donation. + */ + public abstract get amount(): number; +} \ No newline at end of file diff --git a/src/models/Donor.ts b/src/models/Donor.ts new file mode 100644 index 0000000..cb9d9df --- /dev/null +++ b/src/models/Donor.ts @@ -0,0 +1,37 @@ +import { + + IsInt, + + + + IsString +} from "class-validator"; + +/** + * Defines the Donor class. +*/ +export class Donor { + /** + * The donor's id. + */ + @IsInt() + id: number; + + /** + * The donor's first name. + */ + @IsString() + firstname: string; + + /** + * The donor's middle name. + */ + @IsString() + middlename?: string; + + /** + * The donor's last name. + */ + @IsString() + lastname: string; +} diff --git a/src/models/FixedDonation.ts b/src/models/FixedDonation.ts new file mode 100644 index 0000000..18c8a23 --- /dev/null +++ b/src/models/FixedDonation.ts @@ -0,0 +1,16 @@ +import { IsInt, IsPositive } from "class-validator"; +import { Donation } from "./Donation"; + +/** + * Defines the FixedDonation entity. + * In the past there was no easy way to track fixed donations (eg. for creating donation receipts). +*/ +export class FixedDonation extends Donation { + + /** + * The donation's amount in cents (or whatever your currency's smallest unit is.). + */ + @IsInt() + @IsPositive() + amount: number; +} \ No newline at end of file diff --git a/src/models/Runner.ts b/src/models/Runner.ts new file mode 100644 index 0000000..f7968a8 --- /dev/null +++ b/src/models/Runner.ts @@ -0,0 +1,47 @@ +import { + IsInt, + IsObject, + IsString +} from "class-validator"; +import { RunnerGroup } from './RunnerGroup'; + +/** + * Defines the runner class (from which the runner sponsoring contracts get generated). +*/ +export class Runner { + /** + * The runner's id. + */ + @IsInt() + id: number; + + /** + * The runner's first name. + */ + @IsString() + firstname: string; + + /** + * The runner's middle name. + */ + @IsString() + middlename?: string; + + /** + * The runner's last name. + */ + @IsString() + lastname: string; + + /** + * The runner's group. + */ + @IsObject() + group: RunnerGroup; + + /** + * The total distance ran by the runner. + */ + @IsInt() + distance: number; +} diff --git a/src/models/RunnerCard.ts b/src/models/RunnerCard.ts new file mode 100644 index 0000000..3a4cffb --- /dev/null +++ b/src/models/RunnerCard.ts @@ -0,0 +1,33 @@ +import { + IsEAN, + IsInt, + IsNotEmpty, + IsObject, + IsString +} from "class-validator"; +import { Runner } from './Runner'; + +/** + * Defines the runner card class (used to create runner card pdfs). +*/ +export class RunnerCard { + /** + * The cards's id. + */ + @IsInt() + id: number; + + /** + * The card's associated runner. + */ + @IsObject() + runner: Runner | null; + + /** + * The card's code. + */ + @IsEAN() + @IsString() + @IsNotEmpty() + code: string; +} diff --git a/src/models/RunnerGroup.ts b/src/models/RunnerGroup.ts new file mode 100644 index 0000000..f50a179 --- /dev/null +++ b/src/models/RunnerGroup.ts @@ -0,0 +1,36 @@ +import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator"; + +/** + * Defines the runner group class - a simplified version of the backend's ResponseRunnerTeam/-Organization +*/ +export abstract class RunnerGroup { + /** + * The group's id. + */ + @IsInt() + @IsNotEmpty() + id: number;; + + /** + * The group's name. + */ + @IsString() + @IsNotEmpty() + name: string; + + /** + * The group's parent group. + * If it is set this implies that the object is a team. + */ + @IsObject() + @IsOptional() + parentGroup?: RunnerGroup + + /** + * Returns the groups full name in the format: org.name/team.name (or just org). + */ + public get fullName(): string { + if (!this.parentGroup) { return this.name; } + return `${this.name}/${this.parentGroup.fullName}`; + } +} diff --git a/src/templates/.gitkeep b/src/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/templates/sponsoring_contract.html b/src/templates/sponsoring_contract.html new file mode 100644 index 0000000..1597b2c --- /dev/null +++ b/src/templates/sponsoring_contract.html @@ -0,0 +1,45 @@ + + + + Sponsoring contract + + + +
+

{{Runner Name}}

+
+ + \ No newline at end of file