mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
add server to monorepo.
This commit is contained in:
203
packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts
Normal file
203
packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
IItemEntry,
|
||||
ILedgerEntry,
|
||||
AccountNormal,
|
||||
ILedger,
|
||||
} from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceGLEntries {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private ledegrRepository: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Writes a sale invoice GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public writeInvoiceGLEntries = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const saleInvoice = await SaleInvoice.query(trx)
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
// Find or create the A/R account.
|
||||
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode
|
||||
);
|
||||
// Retrieves the ledger of the invoice.
|
||||
const ledger = this.getInvoiceGLedger(saleInvoice, ARAccount.id);
|
||||
|
||||
// Commits the ledger entries to the storage as UOW.
|
||||
await this.ledegrRepository.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the given invoice GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewritesInvoiceGLEntries = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Reverts the invoice GL entries.
|
||||
await this.revertInvoiceGLEntries(tenantId, saleInvoiceId, trx);
|
||||
|
||||
// Writes the invoice GL entries.
|
||||
await this.writeInvoiceGLEntries(tenantId, saleInvoiceId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the given invoice GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertInvoiceGLEntries = async (
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
await this.ledegrRepository.deleteByReference(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
'SaleInvoice',
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given invoice ledger.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
public getInvoiceGLedger = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
): ILedger => {
|
||||
const entries = this.getInvoiceGLEntries(saleInvoice, ARAccountId);
|
||||
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL common entry.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {Partial<ILedgerEntry>}
|
||||
*/
|
||||
private getInvoiceGLCommonEntry = (
|
||||
saleInvoice: ISaleInvoice
|
||||
): Partial<ILedgerEntry> => ({
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
currencyCode: saleInvoice.currencyCode,
|
||||
exchangeRate: saleInvoice.exchangeRate,
|
||||
|
||||
transactionType: 'SaleInvoice',
|
||||
transactionId: saleInvoice.id,
|
||||
|
||||
date: saleInvoice.invoiceDate,
|
||||
userId: saleInvoice.userId,
|
||||
|
||||
transactionNumber: saleInvoice.invoiceNo,
|
||||
referenceNumber: saleInvoice.referenceNo,
|
||||
|
||||
createdAt: saleInvoice.createdAt,
|
||||
indexGroup: 10,
|
||||
|
||||
branchId: saleInvoice.branchId,
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve receivable entry of the given invoice.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceReceivableEntry = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleInvoice.localAmount,
|
||||
accountId: ARAccountId,
|
||||
contactId: saleInvoice.customerId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
} as ILedgerEntry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve item income entry of the given invoice.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @param {IItemEntry} entry -
|
||||
* @param {number} index -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceItemEntry = R.curry(
|
||||
(
|
||||
saleInvoice: ISaleInvoice,
|
||||
entry: IItemEntry,
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
const localAmount = entry.amount * saleInvoice.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: localAmount,
|
||||
accountId: entry.sellAccountId,
|
||||
note: entry.description,
|
||||
index: index + 2,
|
||||
itemId: entry.itemId,
|
||||
itemQuantity: entry.quantity,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
projectId: entry.projectId || saleInvoice.projectId
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} ARAccountId
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
public getInvoiceGLEntries = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const receivableEntry = this.getInvoiceReceivableEntry(
|
||||
saleInvoice,
|
||||
ARAccountId
|
||||
);
|
||||
const transformItemEntry = this.getInvoiceItemEntry(saleInvoice);
|
||||
const creditEntries = saleInvoice.entries.map(transformItemEntry);
|
||||
|
||||
return [receivableEntry, ...creditEntries];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export class InvoicePaymentTransactionTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedPaymentAmount', 'formattedPaymentDate'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentAmount = (entry): string => {
|
||||
return formatNumber(entry.paymentAmount, {
|
||||
currencyCode: entry.payment.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
protected formattedPaymentDate = (entry): string => {
|
||||
return this.formatDate(entry.payment.paymentDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public transform = (entry) => {
|
||||
return {
|
||||
invoiceId: entry.invoiceId,
|
||||
paymentReceiveId: entry.paymentReceiveId,
|
||||
|
||||
paymentDate: entry.payment.paymentDate,
|
||||
formattedPaymentDate: entry.formattedPaymentDate,
|
||||
|
||||
paymentAmount: entry.paymentAmount,
|
||||
formattedPaymentAmount: entry.formattedPaymentAmount,
|
||||
currencyCode: entry.payment.currencyCode,
|
||||
|
||||
paymentNumber: entry.payment.paymentReceiveNo,
|
||||
paymentReferenceNo: entry.payment.referenceNo,
|
||||
|
||||
invoiceNumber: entry.invoice.invoiceNo,
|
||||
invoiceReferenceNo: entry.invoice.referenceNo,
|
||||
|
||||
depositAccountId: entry.payment.depositAccountId,
|
||||
depositAccountName: entry.payment.depositAccount.name,
|
||||
depositAccountSlug: entry.payment.depositAccount.slug,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Knex } from 'knex';
|
||||
import async from 'async';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { PaymentReceiveGLEntries } from '../PaymentReceives/PaymentReceiveGLEntries';
|
||||
|
||||
@Service()
|
||||
export class InvoicePaymentsGLEntriesRewrite {
|
||||
@Inject()
|
||||
public tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
public paymentGLEntries: PaymentReceiveGLEntries;
|
||||
|
||||
/**
|
||||
* Rewrites the payment GL entries task.
|
||||
* @param {{ tenantId: number, paymentId: number, trx: Knex?.Transaction }}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public rewritePaymentsGLEntriesTask = async ({
|
||||
tenantId,
|
||||
paymentId,
|
||||
trx,
|
||||
}) => {
|
||||
await this.paymentGLEntries.rewritePaymentGLEntries(
|
||||
tenantId,
|
||||
paymentId,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the payment GL entries of the given payments ids.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} paymentsIds
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public rewritePaymentsGLEntriesQueue = async (
|
||||
tenantId: number,
|
||||
paymentsIds: number[],
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
// Initiate a new queue for accounts balance mutation.
|
||||
const rewritePaymentGL = async.queue(this.rewritePaymentsGLEntriesTask, 10);
|
||||
|
||||
paymentsIds.forEach((paymentId: number) => {
|
||||
rewritePaymentGL.push({ paymentId, trx, tenantId });
|
||||
});
|
||||
if (paymentsIds.length > 0) {
|
||||
await rewritePaymentGL.drain();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the payments GL entries that associated to the given invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public invoicePaymentsGLEntriesRewrite = async (
|
||||
tenantId: number,
|
||||
invoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoicePaymentEntries = await PaymentReceiveEntry.query().where(
|
||||
'invoiceId',
|
||||
invoiceId
|
||||
);
|
||||
const paymentsIds = invoicePaymentEntries.map((e) => e.paymentReceiveId);
|
||||
|
||||
await this.rewritePaymentsGLEntriesQueue(tenantId, paymentsIds, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { InvoicePaymentTransactionTransformer } from './InvoicePaymentTransactionTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export default class InvoicePaymentsService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve the invoice assocaited payments transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
*/
|
||||
public getInvoicePayments = async (tenantId: number, invoiceId: number) => {
|
||||
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentsEntries = await PaymentReceiveEntry.query()
|
||||
.where('invoiceId', invoiceId)
|
||||
.withGraphJoined('payment.depositAccount')
|
||||
.withGraphJoined('invoice')
|
||||
.orderBy('payment:paymentDate', 'ASC');
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
paymentsEntries,
|
||||
new InvoicePaymentTransactionTransformer()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces';
|
||||
import { increment } from 'utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceCostGLEntries {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Writes journal entries from sales invoices.
|
||||
* @param {number} tenantId - The tenant id.
|
||||
* @param {Date} startingDate - Starting date.
|
||||
* @param {boolean} override
|
||||
*/
|
||||
public writeInventoryCostJournalEntries = async (
|
||||
tenantId: number,
|
||||
startingDate: Date,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
|
||||
|
||||
const inventoryCostLotTrans = await InventoryCostLotTracker.query()
|
||||
.where('direction', 'OUT')
|
||||
.where('transaction_type', 'SaleInvoice')
|
||||
.where('cost', '>', 0)
|
||||
.modify('filterDateRange', startingDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.withGraphFetched('invoice')
|
||||
.withGraphFetched('item');
|
||||
|
||||
const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
|
||||
|
||||
// Commit the ledger to the storage.
|
||||
await this.ledgerStorage.commit(tenantId, ledger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the inventory cost lots ledger.
|
||||
* @param {IInventoryLotCost[]} inventoryCostLots
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
private getInventoryCostLotsLedger = (
|
||||
inventoryCostLots: IInventoryLotCost[]
|
||||
) => {
|
||||
// Groups the inventory cost lots transactions.
|
||||
const inventoryTransactions =
|
||||
groupInventoryTransactionsByTypeId(inventoryCostLots);
|
||||
|
||||
const entries = inventoryTransactions
|
||||
.map(this.getSaleInvoiceCostGLEntries)
|
||||
.flat();
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IInventoryLotCost} inventoryCostLot
|
||||
* @returns {}
|
||||
*/
|
||||
private getInvoiceCostGLCommonEntry = (
|
||||
inventoryCostLot: IInventoryLotCost
|
||||
) => {
|
||||
return {
|
||||
currencyCode: inventoryCostLot.invoice.currencyCode,
|
||||
exchangeRate: inventoryCostLot.invoice.exchangeRate,
|
||||
|
||||
transactionType: inventoryCostLot.transactionType,
|
||||
transactionId: inventoryCostLot.transactionId,
|
||||
|
||||
date: inventoryCostLot.date,
|
||||
indexGroup: 20,
|
||||
costable: true,
|
||||
createdAt: inventoryCostLot.createdAt,
|
||||
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
|
||||
branchId: inventoryCostLot.invoice.branchId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the inventory cost GL entry.
|
||||
* @param {IInventoryLotCost} inventoryLotCost
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getInventoryCostGLEntry = R.curry(
|
||||
(
|
||||
getIndexIncrement,
|
||||
inventoryCostLot: IInventoryLotCost
|
||||
): ILedgerEntry[] => {
|
||||
const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
|
||||
const costAccountId =
|
||||
inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
|
||||
|
||||
// XXX Debit - Cost account.
|
||||
const costEntry = {
|
||||
...commonEntry,
|
||||
debit: inventoryCostLot.cost,
|
||||
accountId: costAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
itemId: inventoryCostLot.itemId,
|
||||
index: getIndexIncrement(),
|
||||
};
|
||||
// XXX Credit - Inventory account.
|
||||
const inventoryEntry = {
|
||||
...commonEntry,
|
||||
credit: inventoryCostLot.cost,
|
||||
accountId: inventoryCostLot.item.inventoryAccountId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
itemId: inventoryCostLot.itemId,
|
||||
index: getIndexIncrement(),
|
||||
};
|
||||
return [costEntry, inventoryEntry];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes journal entries for given sale invoice.
|
||||
* -------
|
||||
* - Cost of goods sold -> Debit -> YYYY
|
||||
* - Inventory assets -> Credit -> YYYY
|
||||
* --------
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {JournalPoster} journal
|
||||
*/
|
||||
public getSaleInvoiceCostGLEntries = (
|
||||
inventoryCostLots: IInventoryLotCost[]
|
||||
): ILedgerEntry[] => {
|
||||
const getIndexIncrement = increment(0);
|
||||
const getInventoryLotEntry =
|
||||
this.getInventoryCostGLEntry(getIndexIncrement);
|
||||
|
||||
return inventoryCostLots.map(getInventoryLotEntry).flat();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces';
|
||||
import { SaleInvoiceCostGLEntries } from '../SaleInvoiceCostGLEntries';
|
||||
|
||||
@Service()
|
||||
export class InvoiceCostGLEntriesSubscriber {
|
||||
@Inject()
|
||||
invoiceCostEntries: SaleInvoiceCostGLEntries;
|
||||
|
||||
/**
|
||||
* Attaches events.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.inventory.onCostLotsGLEntriesWrite,
|
||||
this.writeInvoicesCostEntriesOnCostLotsWritten
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the invoices cost GL entries once the inventory cost lots be written.
|
||||
* @param {IInventoryCostLotsGLEntriesWriteEvent}
|
||||
*/
|
||||
private writeInvoicesCostEntriesOnCostLotsWritten = async ({
|
||||
trx,
|
||||
startingDate,
|
||||
tenantId,
|
||||
}: IInventoryCostLotsGLEntriesWriteEvent) => {
|
||||
await this.invoiceCostEntries.writeInventoryCostJournalEntries(
|
||||
tenantId,
|
||||
startingDate,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { ISaleInvoiceEditingPayload } from '@/interfaces';
|
||||
import { InvoicePaymentsGLEntriesRewrite } from '../InvoicePaymentsGLRewrite';
|
||||
|
||||
@Service()
|
||||
export class InvoicePaymentGLRewriteSubscriber {
|
||||
@Inject()
|
||||
private invoicePaymentsRewriteGLEntries: InvoicePaymentsGLEntriesRewrite;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach = (bus) => {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEdited,
|
||||
this.paymentGLEntriesRewriteOnPaymentEdit
|
||||
);
|
||||
return bus;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes associated invoiceso of payment receive once edit.
|
||||
* @param {ISaleInvoiceEditingPayload} -
|
||||
*/
|
||||
private paymentGLEntriesRewriteOnPaymentEdit = async ({
|
||||
tenantId,
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditingPayload) => {
|
||||
await this.invoicePaymentsRewriteGLEntries.invoicePaymentsGLEntriesRewrite(
|
||||
tenantId,
|
||||
oldSaleInvoice.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user