Merge branch 'dev' into feature/6-documentation
This commit is contained in:
commit
782b41756d
3
.gitignore
vendored
3
.gitignore
vendored
@ -136,4 +136,5 @@ build
|
|||||||
/docs
|
/docs
|
||||||
lib
|
lib
|
||||||
/oss-attribution
|
/oss-attribution
|
||||||
*.tmp
|
*.tmp
|
||||||
|
*.pdf
|
@ -45,6 +45,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"html-pdf": "^2.2.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"routing-controllers": "^0.9.0-alpha.6",
|
"routing-controllers": "^0.9.0-alpha.6",
|
||||||
"routing-controllers-openapi": "^2.2.0"
|
"routing-controllers-openapi": "^2.2.0"
|
||||||
@ -72,4 +73,4 @@
|
|||||||
"publish": false
|
"publish": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
56
src/PdfCreator.ts
Normal file
56
src/PdfCreator.ts
Normal file
@ -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<Pdf> {
|
||||||
|
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<Buffer> {
|
||||||
|
let promise = await new Promise<Buffer>((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<Stream> {
|
||||||
|
let promise = await new Promise<Stream>((resolve, reject) => {
|
||||||
|
this.content.toStream(function (err, stream: Stream) {
|
||||||
|
resolve(stream);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return await promise;
|
||||||
|
}
|
||||||
|
}
|
20
src/controllers/PdfController.ts
Normal file
20
src/controllers/PdfController.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
57
src/errors/AddressErrors.ts
Normal file
57
src/errors/AddressErrors.ts
Normal file
@ -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"
|
||||||
|
}
|
80
src/models/Address.ts
Normal file
80
src/models/Address.ts
Normal file
@ -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(); }
|
||||||
|
}
|
||||||
|
}
|
16
src/models/CertificateRunner.ts
Normal file
16
src/models/CertificateRunner.ts
Normal file
@ -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[];
|
||||||
|
}
|
40
src/models/DistanceDonation.ts
Normal file
40
src/models/DistanceDonation.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/models/Donation.ts
Normal file
32
src/models/Donation.ts
Normal file
@ -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;
|
||||||
|
}
|
37
src/models/Donor.ts
Normal file
37
src/models/Donor.ts
Normal file
@ -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;
|
||||||
|
}
|
16
src/models/FixedDonation.ts
Normal file
16
src/models/FixedDonation.ts
Normal file
@ -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;
|
||||||
|
}
|
47
src/models/Runner.ts
Normal file
47
src/models/Runner.ts
Normal file
@ -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;
|
||||||
|
}
|
33
src/models/RunnerCard.ts
Normal file
33
src/models/RunnerCard.ts
Normal file
@ -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;
|
||||||
|
}
|
36
src/models/RunnerGroup.ts
Normal file
36
src/models/RunnerGroup.ts
Normal file
@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
0
src/templates/.gitkeep
Normal file
0
src/templates/.gitkeep
Normal file
45
src/templates/sponsoring_contract.html
Normal file
45
src/templates/sponsoring_contract.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<title>Sponsoring contract</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Sackers Gothic Std';
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgb(241,241,241);
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
height: 148mm;
|
||||||
|
width: 210mm;
|
||||||
|
display: block;
|
||||||
|
background: white;
|
||||||
|
page-break-after: auto;
|
||||||
|
margin: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<p style="font-size: 100vw;">{{Runner Name}}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user