Compare commits

...

8 Commits

13 changed files with 152 additions and 25 deletions

View File

@@ -2,8 +2,15 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.3.11](https://git.odit.services/lfk/backend/compare/1.3.10...1.3.11)
- feat(RunnerController): add selfservice_links parameter to getRunners method [`a50d72f`](https://git.odit.services/lfk/backend/commit/a50d72f2f5281b8c28ca64a0970161a35a7af95a)
#### [1.3.10](https://git.odit.services/lfk/backend/compare/1.3.9...1.3.10) #### [1.3.10](https://git.odit.services/lfk/backend/compare/1.3.9...1.3.10)
> 11 April 2025
- chore(release): 1.3.10 [`4723d97`](https://git.odit.services/lfk/backend/commit/4723d9738eacd63fb41f23c628fbe4181bd126de)
- feat(RunnerController.getAll): debug created_via query param filter [`1a478bd`](https://git.odit.services/lfk/backend/commit/1a478bd784e01b9d5a1c6635d1004a9535c9a0e9) - feat(RunnerController.getAll): debug created_via query param filter [`1a478bd`](https://git.odit.services/lfk/backend/commit/1a478bd784e01b9d5a1c6635d1004a9535c9a0e9)
#### [1.3.9](https://git.odit.services/lfk/backend/compare/1.3.8...1.3.9) #### [1.3.9](https://git.odit.services/lfk/backend/compare/1.3.8...1.3.9)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-backend", "name": "@odit/lfk-backend",
"version": "1.3.10", "version": "1.3.11",
"main": "src/app.ts", "main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend", "repository": "https://git.odit.services/lfk/backend",
"author": { "author": {

View File

@@ -1,3 +1,4 @@
import consola from 'consola';
import { config as configDotenv } from 'dotenv'; import { config as configDotenv } from 'dotenv';
import { CountryCode } from 'libphonenumber-js'; import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator'; import ValidatorJS from 'validator';
@@ -20,12 +21,15 @@ export const config = {
} }
let errors = 0 let errors = 0
if (typeof config.internal_port !== "number") { if (typeof config.internal_port !== "number") {
consola.error("Error: APP_PORT is not a number")
errors++ errors++
} }
if (typeof config.development !== "boolean") { if (typeof config.development !== "boolean") {
consola.error("Error: NODE_ENV is not a boolean")
errors++ errors++
} }
if (config.mailer_url == "" || config.mailer_key == "") { if (config.mailer_url == "" || config.mailer_key == "") {
consola.error("Error: invalid mailer config")
errors++; errors++;
} }
function getPhoneCodeLocale(): CountryCode { function getPhoneCodeLocale(): CountryCode {

View File

@@ -4,6 +4,7 @@ import { Repository, getConnectionManager } from 'typeorm';
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors'; import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
import { DonorNotFoundError } from '../errors/DonorErrors'; import { DonorNotFoundError } from '../errors/DonorErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateAnonymousDonation } from '../models/actions/create/CreateAnonymousDonation';
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation'; import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation'; import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation'; import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
@@ -11,6 +12,7 @@ import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonatio
import { DistanceDonation } from '../models/entities/DistanceDonation'; import { DistanceDonation } from '../models/entities/DistanceDonation';
import { Donation } from '../models/entities/Donation'; import { Donation } from '../models/entities/Donation';
import { FixedDonation } from '../models/entities/FixedDonation'; import { FixedDonation } from '../models/entities/FixedDonation';
import { ResponseAnonymousDonation } from '../models/responses/ResponseAnonymousDonation';
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation'; import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
import { ResponseDonation } from '../models/responses/ResponseDonation'; import { ResponseDonation } from '../models/responses/ResponseDonation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@@ -35,6 +37,7 @@ export class DonationController {
@Authorized("DONATION:GET") @Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation, { isArray: true }) @ResponseSchema(ResponseDonation, { isArray: true })
@ResponseSchema(ResponseDistanceDonation, { isArray: true }) @ResponseSchema(ResponseDistanceDonation, { isArray: true })
@ResponseSchema(ResponseAnonymousDonation, { isArray: true })
@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' }) @OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonations: ResponseDonation[] = new Array<ResponseDonation>(); let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
@@ -56,6 +59,7 @@ export class DonationController {
@Authorized("DONATION:GET") @Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation) @ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation) @ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(ResponseAnonymousDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 }) @ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@OnUndefined(DonationNotFoundError) @OnUndefined(DonationNotFoundError)
@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' }) @OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
@@ -76,6 +80,17 @@ export class DonationController {
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse(); return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
} }
@Post('/anonymous')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a anonymous donation' })
async postAnonymous(@Body({ validate: true }) createDonation: CreateAnonymousDonation) {
let donation = await createDonation.toEntity();
donation = await this.fixedDonationRepository.save(donation);
return (await this.donationRepository.findOne({ id: donation.id })).toResponse();
}
@Post('/distance') @Post('/distance')
@Authorized("DONATION:CREATE") @Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDistanceDonation) @ResponseSchema(ResponseDistanceDonation)

View File

@@ -62,13 +62,13 @@ export class RunnerOrganizationController {
@ResponseSchema(ResponseRunner, { isArray: true }) @ResponseSchema(ResponseRunner, { isArray: true })
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all runners from this org and it\'s teams (if you don\'t provide the ?onlyDirect=true param). <br> This includes the runner\'s group and distance ran.' }) @OpenAPI({ description: 'Lists all runners from this org and it\'s teams (if you don\'t provide the ?onlyDirect=true param). <br> This includes the runner\'s group and distance ran.' })
async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean) { async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>(); let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
let runners: Runner[]; let runners: Runner[];
if (!onlyDirect) { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.group.parentGroup', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; } if (!onlyDirect) { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.group.parentGroup', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; }
else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; } else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; }
runners.forEach(runner => { runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner)); responseRunners.push(new ResponseRunner(runner, selfservice_links));
}); });
return responseRunners; return responseRunners;
} }

View File

@@ -60,11 +60,11 @@ export class RunnerTeamController {
@ResponseSchema(ResponseRunner, { isArray: true }) @ResponseSchema(ResponseRunner, { isArray: true })
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all runners from this team. <br> This includes the runner\'s group and distance ran.' }) @OpenAPI({ description: 'Lists all runners from this team. <br> This includes the runner\'s group and distance ran.' })
async getRunners(@Param('id') id: number) { async getRunners(@Param('id') id: number, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>(); let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
const runners = (await this.runnerTeamRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; const runners = (await this.runnerTeamRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners;
runners.forEach(runner => { runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner)); responseRunners.push(new ResponseRunner(runner, selfservice_links));
}); });
return responseRunners; return responseRunners;
} }

View File

@@ -0,0 +1,29 @@
import { IsInt, IsPositive } from 'class-validator';
import { FixedDonation } from '../../entities/FixedDonation';
import { CreateDonation } from './CreateDonation';
/**
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateAnonymousDonation extends CreateDonation {
/**
* The donation's amount.
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amount: number;
/**
* Creates a new FixedDonation entity from this.
*/
public async toEntity(): Promise<FixedDonation> {
let newDonation = new FixedDonation;
newDonation.amount = this.amount;
newDonation.paidAmount = this.amount;
return newDonation;
}
}

View File

@@ -10,6 +10,20 @@ import { CreateDonation } from './CreateDonation';
*/ */
export class CreateDistanceDonation extends CreateDonation { export class CreateDistanceDonation extends CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount?: number;
/** /**
* The donation's associated runner's id. * The donation's associated runner's id.
* This is important to link the runner's distance ran to the donation. * This is important to link the runner's distance ran to the donation.

View File

@@ -1,6 +1,5 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator'; import { Exclude } from 'class-transformer';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { DonorNotFoundError } from '../../../errors/DonorErrors';
import { Donation } from '../../entities/Donation'; import { Donation } from '../../entities/Donation';
import { Donor } from '../../entities/Donor'; import { Donor } from '../../entities/Donor';
@@ -8,19 +7,10 @@ import { Donor } from '../../entities/Donor';
* This class is used to create a new Donation entity from a json body (post request). * This class is used to create a new Donation entity from a json body (post request).
*/ */
export abstract class CreateDonation { export abstract class CreateDonation {
/** @Exclude()
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number; donor: number;
/** @Exclude()
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
@IsOptional()
paidAmount?: number; paidAmount?: number;
/** /**
@@ -33,9 +23,6 @@ export abstract class CreateDonation {
*/ */
public async getDonor(): Promise<Donor> { public async getDonor(): Promise<Donor> {
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor }); const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
if (!donor) {
throw new DonorNotFoundError();
}
return donor; return donor;
} }
} }

View File

@@ -6,6 +6,21 @@ import { CreateDonation } from './CreateDonation';
* This class is used to create a new FixedDonation entity from a json body (post request). * This class is used to create a new FixedDonation entity from a json body (post request).
*/ */
export class CreateFixedDonation extends CreateDonation { export class CreateFixedDonation extends CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount?: number;
/** /**
* The donation's amount. * The donation's amount.
* The unit is your currency's smallest unit (default: euro cent). * The unit is your currency's smallest unit (default: euro cent).

View File

@@ -1,6 +1,5 @@
import { import {
IsInt, IsInt
IsNotEmpty
} from "class-validator"; } from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation'; import { ResponseDonation } from '../responses/ResponseDonation';
@@ -24,7 +23,6 @@ export abstract class Donation {
/** /**
* The donations's donor. * The donations's donor.
*/ */
@IsNotEmpty()
@ManyToOne(() => Donor, donor => donor.donations) @ManyToOne(() => Donor, donor => donor.donations)
donor: Donor; donor: Donor;

View File

@@ -84,6 +84,6 @@ export class Runner extends Participant {
* Turns this entity into it's response class. * Turns this entity into it's response class.
*/ */
public toResponse(): ResponseRunner { public toResponse(): ResponseRunner {
return new ResponseRunner(this); return new ResponseRunner(this, true);
} }
} }

View File

@@ -0,0 +1,58 @@
import { IsInt, IsPositive } from "class-validator";
import { Donation } from '../entities/Donation';
import { DonationStatus } from '../enums/DonationStatus';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
/**
* Defines the donation response.
*/
export class ResponseAnonymousDonation implements IResponse {
/**
* The responseType.
* This contains the type of class/entity this response contains.
*/
responseType: ResponseObjectType = ResponseObjectType.DONATION;
/**
* The donation's payment status.
* Provides you with a quick indicator of it's payment status.
*/
status: DonationStatus;
/**
* The donation's id.
*/
@IsInt()
@IsPositive()
id: number;
/**
* The donation's amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
amount: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount: number;
/**
* Creates a ResponseDonation object from a scan.
* @param donation The donation the response shall be build for.
*/
public constructor(donation: Donation) {
this.id = donation.id;
this.amount = donation.amount;
this.paidAmount = donation.paidAmount || 0;
if (this.paidAmount < this.amount) {
this.status = DonationStatus.OPEN;
}
else {
this.status = DonationStatus.PAID;
}
}
}