Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(invoices): Invoices are now considered as a balance top-up #259

Merged
merged 20 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/controller/balance-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class BalanceController extends BaseController {
// eslint-disable-next-line class-methods-use-this
private async getOwnBalance(req: RequestWithToken, res: Response): Promise<void> {
try {
res.json(await BalanceService.getBalance(req.token.user.id));
res.json(await new BalanceService().getBalance(req.token.user.id));
} catch (error) {
this.logger.error(`Could not get balance of user with id ${req.token.user.id}`, error);
res.status(500).json('Internal server error.');
Expand Down Expand Up @@ -137,7 +137,7 @@ export default class BalanceController extends BaseController {
}

try {
const result = await BalanceService.getBalances(params, { take, skip });
const result = await new BalanceService().getBalances(params, { take, skip });
res.json(result);
} catch (error) {
this.logger.error('Could not get balances', error);
Expand All @@ -161,7 +161,7 @@ export default class BalanceController extends BaseController {
try {
const userId = Number.parseInt(req.params.id, 10);
if (await User.findOne({ where: { id: userId, deleted: false } })) {
res.json(await BalanceService.getBalance(userId));
res.json(await new BalanceService().getBalance(userId));
} else {
res.status(404).json('User does not exist');
}
Expand Down
69 changes: 64 additions & 5 deletions src/controller/invoice-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from './request/invoice-request';
import verifyCreateInvoiceRequest, { verifyUpdateInvoiceRequest } from './request/validators/invoice-request-spec';
import { isFail } from '../helpers/specification-validation';
import { asBoolean, asInvoiceState } from '../helpers/validators';
import { asBoolean, asDate, asInvoiceState, asNumber } from '../helpers/validators';
import Invoice from '../entity/invoices/invoice';
import User, { UserType } from '../entity/user/user';
import { UpdateInvoiceUserRequest } from './request/user-request';
Expand Down Expand Up @@ -106,6 +106,12 @@ export default class InvoiceController extends BaseController {
handler: this.getInvoicePDF.bind(this),
},
},
'/eligible-transactions': {
GET: {
policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Invoice', ['*']),
handler: this.getEligibleTransactions.bind(this),
},
},
};
}

Expand Down Expand Up @@ -181,12 +187,19 @@ export default class InvoiceController extends BaseController {
{ invoiceId, returnInvoiceEntries },
);

if (!invoices[0]) {
const invoice = invoices[0];
if (!invoice) {
res.status(404).json('Unknown invoice ID.');
return;
}

res.json(InvoiceService.toResponse(invoices[0], returnInvoiceEntries));
let response;
if (returnInvoiceEntries) {
response = await new InvoiceService().asInvoiceResponse(invoice);
} else {
response = InvoiceService.asBaseInvoiceResponse(invoice);
}
res.json(response);
} catch (error) {
this.logger.error('Could not return invoice:', error);
res.status(500).json('Internal server error.');
Expand Down Expand Up @@ -219,6 +232,7 @@ export default class InvoiceController extends BaseController {
...body,
date: body.date ? new Date(body.date) : new Date(),
byId: body.byId ?? req.token.user.id,
description: body.description ?? '',
};

const validation = await verifyCreateInvoiceRequest(params);
Expand All @@ -227,9 +241,15 @@ export default class InvoiceController extends BaseController {
return;
}

if (params.customEntries) {
res.status(400).json('Custom entries are not supported anymore.');
return;
}


const invoice: Invoice = await AppDataSource.manager.transaction(async (manager) =>
new InvoiceService(manager).createInvoice(params));
res.json(InvoiceService.toResponse(invoice, true));
res.json(await new InvoiceService().asInvoiceResponse(invoice));
} catch (error) {
if (error instanceof NotImplementedError) {
res.status(501).json(error.message);
Expand Down Expand Up @@ -277,7 +297,7 @@ export default class InvoiceController extends BaseController {
const invoice: Invoice = await AppDataSource.manager.transaction(async (manager) =>
new InvoiceService(manager).updateInvoice(params));

res.json(InvoiceService.toResponse(invoice, false));
res.json(InvoiceService.asBaseInvoiceResponse(invoice));
} catch (error) {
this.logger.error('Could not update invoice:', error);
res.status(500).json('Internal server error.');
Expand Down Expand Up @@ -466,6 +486,45 @@ export default class InvoiceController extends BaseController {
}
}

/**
* GET /invoices/eligible-transactions
* @summary Get eligible transactions for invoice creation.
* @operationId getEligibleTransactions
* @tags invoices - Operations of the invoices controller
* @security JWT
* @param {integer} forId.query.required - Filter on Id of the debtor
* @param {string} fromDate.query.required - Start date for selected transactions (inclusive)
* @param {string} tillDate.query - End date for selected transactions (exclusive)
* @return {TransactionResponse} 200 - The eligible transactions
* @return {string} 500 - Internal server error
*/
public async getEligibleTransactions(req: RequestWithToken, res: Response): Promise<void> {
this.logger.trace('Get eligible transactions for invoice creation', req.query, 'by user', req.token.user);

let fromDate, tillDate;
let forId;
try {
forId = asNumber(req.query.forId);
fromDate = asDate(req.query.fromDate);
tillDate = req.query.tillDate ? asDate(req.query.tillDate) : undefined;
} catch (e) {
res.status(400).send(e.message);
return;
}

try {
const transactions = await new InvoiceService().getTransactionsForInvoice({
forId,
fromDate,
tillDate,
});
res.json(transactions);
} catch (error) {
this.logger.error('Could not get eligible transactions:', error);
res.status(500).json('Internal server error.');
}
}


/**
* Function to determine which credentials are needed to get invoice
Expand Down
4 changes: 2 additions & 2 deletions src/controller/payout-request-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export default class PayoutRequestController extends BaseController {
return;
}

const balance = await BalanceService.getBalance(user.id);
const balance = await new BalanceService().getBalance(user.id);
if (balance.amount.amount < body.amount.amount) {
res.status(400).json('Insufficient balance.');
return;
Expand Down Expand Up @@ -241,7 +241,7 @@ export default class PayoutRequestController extends BaseController {
}

if (body.state === PayoutRequestState.APPROVED) {
const balance = await BalanceService.getBalance(payoutRequest.requestedBy.id);
const balance = await new BalanceService().getBalance(payoutRequest.requestedBy.id);
if (balance.amount.amount < payoutRequest.amount.amount) {
res.status(400).json('Insufficient balance.');
return;
Expand Down
23 changes: 13 additions & 10 deletions src/controller/request/invoice-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,24 @@ export interface UpdateInvoiceRequest extends BaseUpdateInvoice {
state?: keyof typeof InvoiceState,
}

export interface InvoiceTransactionsRequest {
forId: number,
fromDate?: Date,
tillDate?: Date,
}

export interface BaseInvoice {
forId: number,
reference: string,
description?: string,
customEntries?: InvoiceEntryRequest[],
transactionIDs?: number[],
fromDate?: string,
isCreditInvoice: boolean,
transactionIDs: number[],
}

export interface CreateInvoiceParams extends BaseInvoice {
byId: number,
street: string;
postalCode:string;
reference: string,
description: string,
city: string;
country: string;
addressee: string,
Expand All @@ -83,13 +87,10 @@ export interface CreateInvoiceParams extends BaseInvoice {
* @property {integer} forId.required - The recipient of the Invoice.
* @property {integer} byId - The creator of the Invoice, defaults to the ID of the requester.
* @property {string} addressee - Name of the addressed, defaults to the fullname of the person being invoiced.
* @property {string} description - The description of the invoice.
* @property {string} description.required - The description of the invoice.
* @property {string} reference.required - The reference of the invoice.
* @property {Array<InvoiceEntryRequest>} customEntries - Custom entries to be added to the invoice
* @property {Array<integer>} transactionIDs - IDs of the transactions to add to the Invoice.
* @property {string} fromDate - For creating an Invoice for all transactions from a specific date.
* @property {boolean} isCreditInvoice.required - If the invoice is an credit Invoice
* If an invoice is a credit invoice the relevant subtransactions are defined as all the sub transactions which have `subTransaction.toId == forId`.
* @property {Array<integer>} transactionIDs.required - IDs of the transactions to add to the Invoice.
* @property {string} street - Street to use on the invoice, overwrites the users default.
* @property {string} postalCode - Postal code to use on the invoice, overwrites the users default.
* @property {string} city - City to use on the invoice, overwrites the users default.
Expand All @@ -106,4 +107,6 @@ export interface CreateInvoiceRequest extends BaseInvoice {
addressee?: string,
date?: Date,
attention?: string,
reference: string,
description: string,
}
41 changes: 18 additions & 23 deletions src/controller/request/validators/invoice-request-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import { In } from 'typeorm';
import { FindOptionsRelations, In } from 'typeorm';
import {
BaseInvoice, CreateInvoiceParams, CreateInvoiceRequest, UpdateInvoiceParams,
} from '../invoice-request';
Expand All @@ -30,11 +30,9 @@ import {
} from '../../../helpers/specification-validation';
import Transaction from '../../../entity/transactions/transaction';
import InvoiceEntryRequest from '../invoice-entry-request';
import { validOrUndefinedDate } from './duration-spec';
import stringSpec from './string-spec';
import { positiveNumber, userMustExist } from './general-validators';
import {
CREDIT_CONTAINS_INVOICE_ACCOUNT,
INVALID_INVOICE_ID,
INVALID_TRANSACTION_IDS,
INVALID_TRANSACTION_OWNER,
Expand All @@ -43,40 +41,38 @@ import {
} from './validation-errors';
import { InvoiceState } from '../../../entity/invoices/invoice-status';
import Invoice from '../../../entity/invoices/invoice';
import { UserType } from '../../../entity/user/user';

/**
* Checks whether all the transactions exists and are credited to the debtor or sold in case of credit Invoice.
*/
async function validTransactionIds<T extends BaseInvoice>(p: T) {
if (!p.transactionIDs) return toPass(p);

const transactions = await Transaction.find({ where: { id: In(p.transactionIDs) }, relations: ['from', 'subTransactions', 'subTransactions.subTransactionRows', 'subTransactions.subTransactionRows.invoice', 'subTransactions.to'] });
const relations: FindOptionsRelations<Transaction> = {
from: true,
subTransactions: {
subTransactionRows: {
invoice: true,
},
to: true,
},
};
const transactions = await Transaction.find({ where: { id: In(p.transactionIDs) },
relations });
let notOwnedByUser = [];
let fromInvoiceAccount: number[] = [];
if (p.isCreditInvoice) {
transactions.forEach((t) => {
t.subTransactions.forEach((tSub) => {
if (tSub.to.id !== p.forId) notOwnedByUser.push(t);
});
});
} else {
notOwnedByUser.push(...transactions.filter((t) => t.from.id !== p.forId));
}
notOwnedByUser.push(...transactions.filter((t) => t.from.id !== p.forId));
if (notOwnedByUser.length !== 0) return toFail(INVALID_TRANSACTION_OWNER());
if (transactions.length !== p.transactionIDs.length) return toFail(INVALID_TRANSACTION_IDS());

const alreadyInvoiced: number[] = [];
transactions.forEach((t) => {
if (t.from.type === UserType.INVOICE) fromInvoiceAccount.push(t.id);
t.subTransactions.forEach((tSub) => {
tSub.subTransactionRows.forEach((tSubRow) => {
if (tSubRow.invoice !== null) alreadyInvoiced.push(tSubRow.id);
});
});
});
if (alreadyInvoiced.length !== 0) return toFail(SUBTRANSACTION_ALREADY_INVOICED(alreadyInvoiced));
if (p.isCreditInvoice && fromInvoiceAccount.length !== 0) return toFail(CREDIT_CONTAINS_INVOICE_ACCOUNT(fromInvoiceAccount));
return toPass(p);
}

Expand All @@ -85,10 +81,10 @@ async function validTransactionIds<T extends BaseInvoice>(p: T) {
* @param p
*/
async function existsAndNotDeleted<T extends UpdateInvoiceParams>(p: T) {
const base: Invoice = await Invoice.findOne({ where: { id: p.invoiceId }, relations: ['latestStatus'] });
const base: Invoice = await Invoice.findOne({ where: { id: p.invoiceId }, relations: ['invoiceStatus'] });

if (!base) return toFail(INVALID_INVOICE_ID());
if (base.latestStatus.state === InvoiceState.DELETED) {
if (base.invoiceStatus[base.invoiceStatus.length - 1].state === InvoiceState.DELETED) {
return toFail(INVOICE_IS_DELETED());
}

Expand All @@ -102,8 +98,8 @@ async function existsAndNotDeleted<T extends UpdateInvoiceParams>(p: T) {
async function differentState<T extends UpdateInvoiceParams>(p: T) {
if (!p.state) return toPass(p);

const base: Invoice = await Invoice.findOne({ where: { id: p.invoiceId }, relations: ['latestStatus'] });
if (base.latestStatus.state === p.state) {
const base: Invoice = await Invoice.findOne({ where: { id: p.invoiceId }, relations: ['invoiceStatus'] });
if (base.invoiceStatus[base.invoiceStatus.length - 1].state === p.state) {
return toFail(SAME_INVOICE_STATE());
}

Expand All @@ -125,8 +121,6 @@ function baseInvoiceRequestSpec<T extends BaseInvoice>(): Specification<T, Valid
return [
validTransactionIds,
[[userMustExist], 'forId', new ValidationError('forId:')],
[[validOrUndefinedDate], 'fromDate', new ValidationError('fromDate:')],
[stringSpec(), 'description', new ValidationError('description:')],
// We have only defined a single item rule, so we use this to apply it to an array,
[[createArrayRule(invoiceEntryRequestSpec())], 'customEntries', new ValidationError('Custom entries:')],
];
Expand All @@ -152,6 +146,7 @@ const createInvoiceRequestSpec: () => Specification<CreateInvoiceParams, Validat
[stringSpec(), 'city', new ValidationError('city:')],
[stringSpec(), 'country', new ValidationError('country:')],
[stringSpec(), 'reference', new ValidationError('reference:')],
[stringSpec(), 'description', new ValidationError('description:')],
];

export default async function verifyCreateInvoiceRequest(
Expand Down
2 changes: 0 additions & 2 deletions src/controller/request/validators/validation-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ export const SAME_INVOICE_STATE = () => new ValidationError('Update state is sam

export const SUBTRANSACTION_ALREADY_INVOICED = (ids: number[]) => new ValidationError(`SubTransactions ${ids}: have already been invoiced.`);

export const CREDIT_CONTAINS_INVOICE_ACCOUNT = (ids: number[]) => new ValidationError(`Credit Invoice must not contain transactions belonging to Invoice Accounts. Relevant transactions: ${ids}`);

export const INVALID_PIN = () => new ValidationError('PIN is not 4 numbers');

export const WEAK_PASSWORD = () => new ValidationError('Password not strong enough.');
Expand Down
Loading
Loading