mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-14 20:00:33 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cfa9123b8 | ||
|
|
bb9614aafb | ||
|
|
302564a56e | ||
|
|
dcfc231d4d | ||
|
|
64080ed678 | ||
|
|
263935d91e | ||
|
|
7f5ffb8da1 | ||
|
|
f07d25edbe | ||
|
|
7c07d6b5ff | ||
|
|
e433f4ad68 | ||
|
|
a79b9caff6 | ||
|
|
2227cead66 | ||
|
|
410c4ea3e2 | ||
|
|
f92acbcbe0 | ||
|
|
c986585cd9 | ||
|
|
250f0a30ef | ||
|
|
84b5e1adc1 | ||
|
|
2ab28370db | ||
|
|
a88a525326 | ||
|
|
fce8e2c5a4 | ||
|
|
095608266c | ||
|
|
0ec8aaa330 | ||
|
|
9fcb3ef77d | ||
|
|
dc61c57daf | ||
|
|
af284f3f6d | ||
|
|
c43123db76 | ||
|
|
ebbcab3926 | ||
|
|
a235f573c0 | ||
|
|
84a0b8f495 | ||
|
|
b87321c897 | ||
|
|
fc6ebfea5c | ||
|
|
c9fe6d9b37 | ||
|
|
161d60393a | ||
|
|
79413fa85e | ||
|
|
58552c6c94 | ||
|
|
2072e35cfa | ||
|
|
1eaac9d691 | ||
|
|
a916e8a0cb | ||
|
|
a56f560036 | ||
|
|
0fb886936c | ||
|
|
670136916f | ||
|
|
768297f137 | ||
|
|
ef505a0a62 | ||
|
|
42d40620ec | ||
|
|
ee2d8d3065 | ||
|
|
959ef7a691 | ||
|
|
60f03f534b | ||
|
|
e44ebb700a | ||
|
|
8e94c7a755 | ||
|
|
3a2ca36c07 | ||
|
|
88ece74c8a | ||
|
|
67a8610328 | ||
|
|
278d61ce61 | ||
|
|
e72d6ad6b8 | ||
|
|
bf66b31679 | ||
|
|
b4d426d2e8 | ||
|
|
7f7dd270e7 | ||
|
|
f6bad8fe30 | ||
|
|
820b363f79 | ||
|
|
07740a51ef | ||
|
|
d15fb6fe19 | ||
|
|
5749ccec81 | ||
|
|
4a99f6c0cf | ||
|
|
59f480f9d5 | ||
|
|
6cb9c919b5 | ||
|
|
1062b65b5b | ||
|
|
bf3a70cabd | ||
|
|
f46cd28f87 | ||
|
|
dffcfe50aa | ||
|
|
fac55efbc7 | ||
|
|
8b90ce5f6c | ||
|
|
705b8da053 | ||
|
|
4a05ccc692 | ||
|
|
3200d65d90 | ||
|
|
3f23038227 | ||
|
|
408c807fc2 | ||
|
|
d29079a8c5 | ||
|
|
cca596b4a9 | ||
|
|
b768f18294 | ||
|
|
fed620505d | ||
|
|
a008aea3f3 | ||
|
|
25297bc191 | ||
|
|
df8b68fda6 |
@@ -89,3 +89,7 @@ S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_ENDPOINT=
|
||||
S3_BUCKET=
|
||||
|
||||
# PostHog
|
||||
POSTHOG_API_KEY=
|
||||
POSTHOG_HOST=
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
## [0.19.4] - 18-08-2024
|
||||
|
||||
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
|
||||
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
|
||||
* fix: Transaction type and description do not show in general ledger. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/596
|
||||
* fix: Refresh accounts and account transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/597
|
||||
* fix: Typo payments made by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/598
|
||||
* fix: Typo categories list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/600
|
||||
* fix: Autofill the quick created customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/601
|
||||
* fix: Remove views tabs from receipts list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/602
|
||||
* fix: Typo payment receive messages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/599
|
||||
* fix: Enhance Dropzone visual of accept and reject modes by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/603
|
||||
* fix: Matching bank transactions should create associate payment transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/606
|
||||
* fix: Change Dropzone title and subtitle by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/607
|
||||
* fix: Inconsistance page size of paginated data tables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/604
|
||||
* fix: Database connection lost error by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/611
|
||||
* fix: Language typos by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/613
|
||||
* Fix: Correctly display Date, Published At, and Created At in ExpenseDrawerHeader by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/612
|
||||
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
|
||||
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
|
||||
|
||||
## [v0.18.0] - 10-08-2024
|
||||
|
||||
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"agendash": "^3.1.0",
|
||||
"app-root-path": "^3.0.0",
|
||||
"async": "^3.2.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.6.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -98,6 +99,7 @@
|
||||
"objection-unique": "^1.2.2",
|
||||
"plaid": "^10.3.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-node": "^4.2.0",
|
||||
"pug": "^3.0.2",
|
||||
"puppeteer": "^10.2.0",
|
||||
"qim": "0.0.52",
|
||||
|
||||
@@ -46,6 +46,10 @@ export class ExcludeBankTransactionsController extends BaseController {
|
||||
query('account_id').optional().isNumeric().toInt(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
|
||||
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
|
||||
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
|
||||
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.getExcludedBankTransactions.bind(this)
|
||||
|
||||
@@ -21,6 +21,10 @@ export class RecognizedTransactionsController extends BaseController {
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('account_id').optional().isNumeric().toInt(),
|
||||
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
|
||||
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
|
||||
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
|
||||
query('max_amount').optional({ nullable: true }).isFloat().isFloat(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.getRecognizedTransactions.bind(this)
|
||||
|
||||
@@ -84,6 +84,10 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
|
||||
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
|
||||
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
|
||||
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { body } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication';
|
||||
import config from '@/config';
|
||||
@Service()
|
||||
export class OneClickDemoController extends BaseController {
|
||||
@Inject()
|
||||
private oneClickDemoApp: OneClickDemoApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
// Protects the endpoints if the feature is not enabled.
|
||||
const protectMiddleware = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
// Add your protection logic here
|
||||
if (config.oneClickDemoAccounts) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).send({ message: 'Forbidden' });
|
||||
}
|
||||
};
|
||||
router.post(
|
||||
'/one_click',
|
||||
protectMiddleware,
|
||||
asyncMiddleware(this.oneClickDemo.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/one_click_signin',
|
||||
[body('demo_id').exists()],
|
||||
this.validationResult,
|
||||
protectMiddleware,
|
||||
asyncMiddleware(this.oneClickSignIn.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-click demo application.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private async oneClickDemo(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.oneClickDemoApp.createOneClick();
|
||||
|
||||
return res.status(200).send({
|
||||
data,
|
||||
message: 'The one-click demo has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign-in to one-click demo account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async oneClickSignIn(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { demoId } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const data = await this.oneClickDemoApp.autoSignIn(demoId);
|
||||
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export class SubscriptionController extends BaseController {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
|
||||
await this.subscriptionApp.cancelSubscription(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
|
||||
@@ -35,7 +35,7 @@ export class Webhooks extends BaseController {
|
||||
*/
|
||||
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
|
||||
const data = req.body;
|
||||
const signature = req.headers['x-signature'] ?? '';
|
||||
const signature = req.headers['x-signature'] as string ?? '';
|
||||
const rawBody = req.rawBody;
|
||||
|
||||
try {
|
||||
|
||||
@@ -63,6 +63,7 @@ import { BankingController } from './controllers/Banking/BankingController';
|
||||
import { Webhooks } from './controllers/Webhooks/Webhooks';
|
||||
import { ExportController } from './controllers/Export/ExportController';
|
||||
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
|
||||
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
@@ -80,6 +81,7 @@ export default () => {
|
||||
app.use('/jobs', Container.get(Jobs).router());
|
||||
app.use('/account', Container.get(Account).router());
|
||||
app.use('/webhooks', Container.get(Webhooks).router());
|
||||
app.use('/demo', Container.get(OneClickDemoController).router())
|
||||
|
||||
// - Dashboard routes.
|
||||
// ---------------------------
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import SettingsStore from '@/services/Settings/SettingsStore';
|
||||
|
||||
|
||||
export default async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { tenantId } = req.user;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
const settings = await initializeTenantSettings(tenantId);
|
||||
req.settings = settings;
|
||||
|
||||
res.on('finish', async () => {
|
||||
await settings.save();
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
export const initializeTenantSettings = async (tenantId: number) => {
|
||||
const tenantContainer = Container.of(`tenant-${tenantId}`);
|
||||
|
||||
if (tenantContainer && !tenantContainer.has('settings')) {
|
||||
@@ -18,10 +28,5 @@ export default async (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
await settings.load();
|
||||
|
||||
req.settings = settings;
|
||||
|
||||
res.on('finish', async () => {
|
||||
await settings.save();
|
||||
});
|
||||
next();
|
||||
return settings;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Container } from 'typedi';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
const SupportedMethods = ['POST', 'PUT'];
|
||||
|
||||
export default (subscriptionSlug = 'main') =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { tenant, tenantId } = req;
|
||||
@@ -19,8 +21,10 @@ export default (subscriptionSlug = 'main') =>
|
||||
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
|
||||
});
|
||||
}
|
||||
// Validate in case the subscription is inactive.
|
||||
else if (subscription.inactive()) {
|
||||
const isMethodSupported = SupportedMethods.includes(req.method);
|
||||
const isSubscriptionInactive = subscription.inactive();
|
||||
|
||||
if (isMethodSupported && isSubscriptionInactive) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Request } from 'express';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
||||
import rtlDetect from 'rtl-detect';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
export default (req: Request, tenant: ITenant) => {
|
||||
const { id: tenantId, organizationId } = tenant;
|
||||
@@ -16,7 +17,7 @@ export default (req: Request, tenant: ITenant) => {
|
||||
|
||||
const tenantContainer = tenantServices.tenantContainer(tenantId);
|
||||
|
||||
tenantContainer.set('i18n', injectI18nUtils(req));
|
||||
tenantContainer.set('i18n', injectI18nUtils());
|
||||
|
||||
const knexInstance = tenantServices.knex(tenantId);
|
||||
const models = tenantServices.models(tenantId);
|
||||
@@ -33,14 +34,35 @@ export default (req: Request, tenant: ITenant) => {
|
||||
};
|
||||
|
||||
export const injectI18nUtils = (req) => {
|
||||
const locale = req.getLocale();
|
||||
const globalI18n = Container.get('i18n');
|
||||
const locale = globalI18n.getLocale();
|
||||
const direction = rtlDetect.getLangDir(locale);
|
||||
|
||||
return {
|
||||
locale,
|
||||
__: req.__,
|
||||
__: globalI18n.__,
|
||||
direction,
|
||||
isRtl: direction === 'rtl',
|
||||
isLtr: direction === 'ltr',
|
||||
};
|
||||
};
|
||||
|
||||
export const initalizeTenantServices = async (tenantId: number) => {
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const tenantServices = Container.get(TenancyService);
|
||||
const tenantsManager = Container.get(TenantsManagerService);
|
||||
|
||||
// Initialize the knex instance.
|
||||
tenantsManager.setupKnexInstance(tenant);
|
||||
|
||||
const tenantContainer = tenantServices.tenantContainer(tenantId);
|
||||
tenantContainer.set('i18n', injectI18nUtils());
|
||||
|
||||
tenantServices.knex(tenantId);
|
||||
tenantServices.models(tenantId);
|
||||
tenantServices.repositories(tenantId);
|
||||
tenantServices.cache(tenantId);
|
||||
};
|
||||
|
||||
@@ -245,4 +245,20 @@ module.exports = {
|
||||
loops: {
|
||||
apiKey: process.env.LOOPS_API_KEY,
|
||||
},
|
||||
|
||||
/**
|
||||
* One-click demo accounts.
|
||||
*/
|
||||
oneClickDemoAccounts: {
|
||||
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
|
||||
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
|
||||
},
|
||||
|
||||
/**
|
||||
* PostHog
|
||||
*/
|
||||
posthog: {
|
||||
apiKey: process.env.POSTHOG_API_KEY,
|
||||
host: process.env.POSTHOG_HOST
|
||||
}
|
||||
};
|
||||
|
||||
43
packages/server/src/constants/event-tracker.ts
Normal file
43
packages/server/src/constants/event-tracker.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export const SALE_INVOICE_CREATED = 'Sale invoice created';
|
||||
export const SALE_INVOICE_EDITED = 'Sale invoice d';
|
||||
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
|
||||
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
||||
|
||||
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
|
||||
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
|
||||
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
|
||||
|
||||
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
|
||||
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
|
||||
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
|
||||
|
||||
export const BILL_CREATED = 'Bill created';
|
||||
export const BILL_EDITED = 'Bill edited';
|
||||
export const BILL_DELETED = 'Bill deleted';
|
||||
|
||||
export const PAYMENT_MADE_CREATED = 'Payment made created';
|
||||
export const PAYMENT_MADE_EDITED = 'Payment made edited';
|
||||
export const PAYMENT_MADE_DELETED = 'Payment made deleted';
|
||||
|
||||
export const EXPENSE_CREATED = 'Expense created';
|
||||
export const EXPENSE_EDITED = 'Expense edited';
|
||||
export const EXPENSE_DELETED = 'Expense deleted';
|
||||
|
||||
export const ACCOUNT_CREATED = 'Account created';
|
||||
export const ACCOUNT_EDITED = 'Account Edited';
|
||||
export const ACCOUNT_DELETED = 'Account deleted';
|
||||
|
||||
export const ITEM_EVENT_CREATED = 'Item created';
|
||||
export const ITEM_EVENT_EDITED = 'Item edited';
|
||||
export const ITEM_EVENT_DELETED = 'Item deleted';
|
||||
|
||||
export const AUTH_SIGNED_UP = 'Auth Signed-up';
|
||||
export const AUTH_RESET_PASSWORD = 'Auth reset password';
|
||||
|
||||
export const ACCOUNT_GROUP = 'Account';
|
||||
export const ITEM_GROUP = 'Item';
|
||||
export const AUTH_GROUP = 'Auth';
|
||||
export const SALE_GROUP = 'Sale';
|
||||
export const PAYMENT_GROUP = 'Payment';
|
||||
export const BILL_GROUP = 'Bill';
|
||||
export const EXPENSE_GROUP = 'Expense';
|
||||
@@ -15,6 +15,7 @@ export default class SeedSettings extends TenantSeeder {
|
||||
|
||||
// Manual journals settings.
|
||||
{ group: 'manual_journals', key: 'next_number', value: '00001' },
|
||||
{ group: 'manual_journals', key: 'number_prefix', value: 'J-' },
|
||||
{ group: 'manual_journals', key: 'auto_increment', value: true },
|
||||
|
||||
// Sale invoices settings.
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface IRegisterDTO {
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
organizationName: string;
|
||||
}
|
||||
|
||||
export interface ILoginDTO {
|
||||
@@ -77,6 +76,10 @@ export interface IAuthSendedResetPassword {
|
||||
|
||||
export interface IAuthGetMetaPOJO {
|
||||
signupDisabled: boolean;
|
||||
oneClickDemo: {
|
||||
enable: boolean;
|
||||
demoUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAuthSignUpVerifingEventPayload {
|
||||
|
||||
@@ -167,11 +167,18 @@ export interface CategorizeTransactionAsExpenseDTO {
|
||||
export interface IGetUncategorizedTransactionsQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface IGetRecognizedTransactionsQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
accountId?: number;
|
||||
}
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IDynamicListFilter, IItemEntry, IVendorCredit } from '@/interfaces';
|
||||
import { IDynamicListFilter, IItemEntry } from '@/interfaces';
|
||||
import { ILedgerEntry } from './Ledger';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
|
||||
export interface ICreditNoteEntryNewDTO {
|
||||
index: number;
|
||||
index?: number;
|
||||
itemId: number;
|
||||
rate: number;
|
||||
quantity: number;
|
||||
@@ -22,7 +22,7 @@ export interface ICreditNoteNewDTO {
|
||||
entries: ICreditNoteEntryNewDTO[];
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
attachments?: AttachmentLinkDTO[]
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface ICreditNoteEditDTO {
|
||||
@@ -35,7 +35,7 @@ export interface ICreditNoteEditDTO {
|
||||
entries: ICreditNoteEntryNewDTO[];
|
||||
branchId?: number;
|
||||
warehouseId?: number;
|
||||
attachments?: AttachmentLinkDTO[]
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface ICreditNoteEntry extends IItemEntry {}
|
||||
@@ -61,7 +61,7 @@ export interface ICreditNote {
|
||||
localAmount?: number;
|
||||
branchId?: number;
|
||||
warehouseId: number;
|
||||
createdAt?: Date,
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export enum CreditNoteAction {
|
||||
|
||||
@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
||||
|
||||
export interface IImportFileCommitedEventPayload {
|
||||
tenantId: number;
|
||||
importId: number;
|
||||
importId: string;
|
||||
meta: ImportFilePreviewPOJO;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export interface IItemEntry {
|
||||
|
||||
export interface IItemEntryDTO {
|
||||
id?: number;
|
||||
index?: number;
|
||||
itemId: number;
|
||||
landedCost?: boolean;
|
||||
warehouseId?: number;
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface IPaymentReceivedEntry {
|
||||
|
||||
export interface IPaymentReceivedEntryDTO {
|
||||
id?: number;
|
||||
index: number;
|
||||
index?: number;
|
||||
paymentReceiveId?: number;
|
||||
invoiceId: number;
|
||||
paymentAmount: number;
|
||||
@@ -74,6 +74,7 @@ export interface IPaymentReceivedEntryDTO {
|
||||
|
||||
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string;
|
||||
filterQuery?: (trx: Knex.Transaction) => void;
|
||||
}
|
||||
|
||||
export interface IPaymentReceivePageEntry {
|
||||
|
||||
@@ -33,3 +33,7 @@ export interface IOrganizationBuildEventPayload {
|
||||
buildDTO: IOrganizationBuildDTO;
|
||||
systemUser: ISystemUser;
|
||||
}
|
||||
|
||||
export interface IOrganizationBuiltEventPayload {
|
||||
tenantId: number;
|
||||
}
|
||||
8
packages/server/src/interfaces/Subscription.ts
Normal file
8
packages/server/src/interfaces/Subscription.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface SubscriptionPayload {
|
||||
lemonSqueezyId?: string;
|
||||
}
|
||||
|
||||
export enum SubscriptionPaymentStatus {
|
||||
Succeed = 'succeed',
|
||||
Failed = 'failed',
|
||||
}
|
||||
@@ -75,6 +75,7 @@ export * from './Times';
|
||||
export * from './ProjectProfitabilitySummary';
|
||||
export * from './TaxRate';
|
||||
export * from './Plaid';
|
||||
export * from './Subscription';
|
||||
|
||||
export interface I18nService {
|
||||
__: (input: string) => string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||
import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
|
||||
import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';
|
||||
|
||||
export class Transformer {
|
||||
public context: any;
|
||||
@@ -155,19 +156,35 @@ export class Transformer {
|
||||
this.dateFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date.
|
||||
* @param {} date
|
||||
* @param {string} format -
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatDate(date, format?: string) {
|
||||
// Use the export date format if the async operation is in exporting,
|
||||
// otherwise use the given or default format.
|
||||
const _format = this.context.exportAls.isExport
|
||||
? EXPORT_DTE_FORMAT
|
||||
: format || this.dateFormat;
|
||||
|
||||
return date ? moment(date).format(_format) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param date
|
||||
* @returns
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatDate(date) {
|
||||
return date ? moment(date).format(this.dateFormat) : '';
|
||||
protected formatDateFromNow(date) {
|
||||
return date ? moment(date).fromNow(true) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param number
|
||||
* @returns
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatNumber(number, props?) {
|
||||
return formatNumber(number, { money: false, ...props });
|
||||
@@ -177,7 +194,7 @@ export class Transformer {
|
||||
*
|
||||
* @param money
|
||||
* @param options
|
||||
* @returns
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatMoney(money, options?) {
|
||||
return formatNumber(money, {
|
||||
|
||||
@@ -3,12 +3,17 @@ import { isNull } from 'lodash';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import { Transformer } from './Transformer';
|
||||
import { ImportAls } from '@/services/Import/ImportALS';
|
||||
import { ExportAls } from '@/services/Export/ExportAls';
|
||||
|
||||
@Service()
|
||||
export class TransformerInjectable {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private exportAls: ExportAls;
|
||||
|
||||
/**
|
||||
* Retrieves the application context of all tenant transformers.
|
||||
* @param {number} tenantId
|
||||
@@ -17,10 +22,12 @@ export class TransformerInjectable {
|
||||
async getApplicationContext(tenantId: number) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const organization = await TenantMetadata.query().findOne({ tenantId });
|
||||
const exportAls = this.exportAls;
|
||||
|
||||
return {
|
||||
organization,
|
||||
i18n,
|
||||
exportAls,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,9 @@ import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashfl
|
||||
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
||||
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
||||
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
|
||||
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
|
||||
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
|
||||
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -246,8 +249,10 @@ export const susbcribers = () => {
|
||||
DeleteCashflowTransactionOnUncategorize,
|
||||
PreventDeleteTransactionOnDelete,
|
||||
|
||||
// Subscription
|
||||
SubscribeFreeOnSignupCommunity,
|
||||
SendVerfiyMailOnSignUp,
|
||||
TriggerInvalidateCacheOnSubscriptionChange,
|
||||
|
||||
// Attachments
|
||||
AttachmentsOnSaleInvoiceCreated,
|
||||
@@ -281,6 +286,11 @@ export const susbcribers = () => {
|
||||
DeleteUncategorizedTransactionsOnAccountDeleting,
|
||||
|
||||
// Loops
|
||||
LoopsEventsSubscriber
|
||||
LoopsEventsSubscriber,
|
||||
|
||||
// Demo Account
|
||||
SeedInitialDemoAccountDataOnOrgBuild,
|
||||
|
||||
...EventsTrackerListeners
|
||||
];
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { I18n } from 'i18n';
|
||||
|
||||
export default () => new I18n({
|
||||
locales: ['en', 'ar'],
|
||||
defaultLocale: 'en',
|
||||
register: global,
|
||||
directory: global.__locales_dir,
|
||||
updateFiles: false,
|
||||
|
||||
@@ -5,6 +5,8 @@ export default {
|
||||
sortField: 'bill_date',
|
||||
},
|
||||
exportable: true,
|
||||
exportFlattenOn: 'entries',
|
||||
|
||||
importable: true,
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
@@ -77,7 +79,7 @@ export default {
|
||||
paymentDate: {
|
||||
name: 'bill_payment.field.payment_date',
|
||||
type: 'date',
|
||||
accessor: 'formattedPaymentDate'
|
||||
accessor: 'formattedPaymentDate',
|
||||
},
|
||||
paymentNumber: {
|
||||
name: 'bill_payment.field.payment_number',
|
||||
@@ -111,6 +113,34 @@ export default {
|
||||
name: 'bill_payment.field.reference',
|
||||
type: 'text',
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
accessor: 'entries',
|
||||
type: 'collection',
|
||||
collectionOf: 'object',
|
||||
columns: {
|
||||
date: {
|
||||
name: 'Bill date',
|
||||
accessor: 'bill.formattedBillDate',
|
||||
},
|
||||
billNo: {
|
||||
name: 'Bill No.',
|
||||
accessor: 'bill.billNo',
|
||||
},
|
||||
billRefNo: {
|
||||
name: 'Bill Reference No.',
|
||||
accessor: 'bill.referenceNo',
|
||||
},
|
||||
billAmount: {
|
||||
name: 'Bill Amount',
|
||||
accessor: 'bill.totalFormatted',
|
||||
},
|
||||
paidAmount: {
|
||||
name: 'Paid Amount',
|
||||
accessor: 'paymentAmountFormatted',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
vendorId: {
|
||||
|
||||
@@ -95,6 +95,11 @@ export default {
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
customerType: {
|
||||
name: 'Customer Type',
|
||||
type: 'text',
|
||||
accessor: 'formattedCustomerType',
|
||||
},
|
||||
firstName: {
|
||||
name: 'vendor.field.first_name',
|
||||
type: 'text',
|
||||
@@ -135,116 +140,117 @@ export default {
|
||||
openingBalance: {
|
||||
name: 'vendor.field.opening_balance',
|
||||
type: 'number',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
openingBalanceAt: {
|
||||
name: 'vendor.field.opening_balance_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
printable: false,
|
||||
accessor: 'formattedOpeningBalanceAt'
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'vendor.field.currency',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
status: {
|
||||
name: 'vendor.field.status',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
note: {
|
||||
name: 'vendor.field.note',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
// Billing Address
|
||||
billingAddress1: {
|
||||
name: 'Billing Address 1',
|
||||
column: 'billing_address1',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddress2: {
|
||||
name: 'Billing Address 2',
|
||||
column: 'billing_address2',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressCity: {
|
||||
name: 'Billing Address City',
|
||||
column: 'billing_address_city',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressCountry: {
|
||||
name: 'Billing Address Country',
|
||||
column: 'billing_address_country',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressPostcode: {
|
||||
name: 'Billing Address Postcode',
|
||||
column: 'billing_address_postcode',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressState: {
|
||||
name: 'Billing Address State',
|
||||
column: 'billing_address_state',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressPhone: {
|
||||
name: 'Billing Address Phone',
|
||||
column: 'billing_address_phone',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
// Shipping Address
|
||||
shippingAddress1: {
|
||||
name: 'Shipping Address 1',
|
||||
column: 'shipping_address1',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddress2: {
|
||||
name: 'Shipping Address 2',
|
||||
column: 'shipping_address2',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressCity: {
|
||||
name: 'Shipping Address City',
|
||||
column: 'shipping_address_city',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressCountry: {
|
||||
name: 'Shipping Address Country',
|
||||
column: 'shipping_address_country',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressPostcode: {
|
||||
name: 'Shipping Address Postcode',
|
||||
column: 'shipping_address_postcode',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressPhone: {
|
||||
name: 'Shipping Address Phone',
|
||||
column: 'shipping_address_phone',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressState: {
|
||||
name: 'Shipping Address State',
|
||||
column: 'shipping_address_state',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
createdAt: {
|
||||
name: 'vendor.field.created_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -41,17 +41,17 @@ export default {
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
sell_price: {
|
||||
name: 'item.field.cost_price',
|
||||
name: 'item.field.sell_price',
|
||||
column: 'sell_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
cost_price: {
|
||||
name: 'item.field.cost_account',
|
||||
name: 'item.field.cost_price',
|
||||
column: 'cost_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
cost_account: {
|
||||
name: 'item.field.sell_account',
|
||||
name: 'item.field.cost_account',
|
||||
column: 'cost_account_id',
|
||||
fieldType: 'relation',
|
||||
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
relationEntityKey: 'slug',
|
||||
},
|
||||
sell_account: {
|
||||
name: 'item.field.sell_description',
|
||||
name: 'item.field.sell_account',
|
||||
column: 'sell_account_id',
|
||||
fieldType: 'relation',
|
||||
|
||||
@@ -155,24 +155,24 @@ export default {
|
||||
printable: false,
|
||||
},
|
||||
sellPrice: {
|
||||
name: 'item.field.cost_price',
|
||||
name: 'item.field.sell_price',
|
||||
type: 'number',
|
||||
exportable: true,
|
||||
},
|
||||
costPrice: {
|
||||
name: 'item.field.cost_account',
|
||||
name: 'item.field.cost_price',
|
||||
type: 'number',
|
||||
exportable: true,
|
||||
},
|
||||
costAccount: {
|
||||
name: 'item.field.sell_account',
|
||||
name: 'item.field.cost_account',
|
||||
type: 'text',
|
||||
accessor: 'costAccount.name',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
sellAccount: {
|
||||
name: 'item.field.sell_description',
|
||||
name: 'item.field.sell_account',
|
||||
type: 'text',
|
||||
accessor: 'sellAccount.name',
|
||||
exportable: true,
|
||||
@@ -257,25 +257,25 @@ export default {
|
||||
name: 'item.field.sell_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
cost_price: {
|
||||
costPrice: {
|
||||
name: 'item.field.cost_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
costAccount: {
|
||||
costAccountId: {
|
||||
name: 'item.field.cost_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
sellAccount: {
|
||||
sellAccountId: {
|
||||
name: 'item.field.sell_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
inventoryAccount: {
|
||||
inventoryAccountId: {
|
||||
name: 'item.field.inventory_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export default {
|
||||
importable: true,
|
||||
|
||||
exportable: true,
|
||||
exportFlattenOn: 'entries',
|
||||
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'paymentReceiveNo',
|
||||
@@ -72,7 +75,7 @@ export default {
|
||||
amount: {
|
||||
name: 'payment_receive.field.amount',
|
||||
type: 'number',
|
||||
accessor: 'formattedAmount'
|
||||
accessor: 'formattedAmount',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'payment_receive.field.reference_no',
|
||||
@@ -92,6 +95,34 @@ export default {
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
accessor: 'entries',
|
||||
type: 'collection',
|
||||
collectionOf: 'object',
|
||||
columns: {
|
||||
date: {
|
||||
name: 'Invoice date',
|
||||
accessor: 'invoice.invoiceDateFormatted',
|
||||
},
|
||||
invoiceNo: {
|
||||
name: 'Invoice No.',
|
||||
accessor: 'invoice.invoiceNo',
|
||||
},
|
||||
invoiceRefNo: {
|
||||
name: 'Invoice Reference No.',
|
||||
accessor: 'invoice.referenceNo',
|
||||
},
|
||||
invoiceAmount: {
|
||||
name: 'Invoice Amount',
|
||||
accessor: 'invoice.totalFormatted',
|
||||
},
|
||||
paidAmount: {
|
||||
name: 'Paid Amount',
|
||||
accessor: 'paymentAmountFormatted',
|
||||
},
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
name: 'payment_receive.field.created_at',
|
||||
type: 'date',
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
importAggregateBy: 'estimateNumber',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Sale Estimates'
|
||||
pageTitle: 'Sale Estimates',
|
||||
},
|
||||
|
||||
fields: {
|
||||
@@ -142,6 +142,7 @@ export default {
|
||||
delivered: {
|
||||
name: 'Delivered',
|
||||
type: 'boolean',
|
||||
accessor: 'isDelivered',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
|
||||
@@ -155,6 +155,7 @@ export default {
|
||||
name: 'invoice.field.delivered',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
accessor: 'isDelivered',
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable global-require */
|
||||
import moment from 'moment';
|
||||
import { Model, mixin } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSettings from './ModelSetting';
|
||||
import Account from './Account';
|
||||
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
|
||||
|
||||
export default class UncategorizedCashflowTransaction extends mixin(
|
||||
@@ -166,6 +166,28 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
pending(query) {
|
||||
query.where('pending', true);
|
||||
},
|
||||
|
||||
minAmount(query, minAmount) {
|
||||
query.where('amount', '>=', minAmount);
|
||||
},
|
||||
|
||||
maxAmount(query, maxAmount) {
|
||||
query.where('amount', '<=', maxAmount);
|
||||
},
|
||||
|
||||
toDate(query, toDate) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const _toDate = moment(toDate).endOf('day').format(dateFormat);
|
||||
|
||||
query.where('date', '<=', _toDate);
|
||||
},
|
||||
|
||||
fromDate(query, fromDate) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const _fromDate = moment(fromDate).startOf('day').format(dateFormat);
|
||||
|
||||
query.where('date', '>=', _fromDate);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { AccountsApplication } from './AccountsApplication';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class AccountsExportable extends Exportable {
|
||||
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
|
||||
inactiveMode: false,
|
||||
...query,
|
||||
structure: IAccountsStructureType.Flat,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
page: 1,
|
||||
} as IAccountsFilter;
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ export class GetAuthMeta {
|
||||
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||
return {
|
||||
signupDisabled: config.signupRestrictions.disabled,
|
||||
oneClickDemo: {
|
||||
enable: config.oneClickDemoAccounts.enable,
|
||||
demoUrl: config.oneClickDemoAccounts.demoUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ExcludedBankTransactionsQuery } from './_types';
|
||||
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
@@ -39,6 +40,18 @@ export class GetExcludedBankTransactionsService {
|
||||
if (_query.accountId) {
|
||||
q.where('account_id', _query.accountId);
|
||||
}
|
||||
if (_query.minDate) {
|
||||
q.modify('fromDate', _query.minDate);
|
||||
}
|
||||
if (_query.maxDate) {
|
||||
q.modify('toDate', _query.maxDate);
|
||||
}
|
||||
if (_query.minAmount) {
|
||||
q.modify('minAmount', _query.minAmount);
|
||||
}
|
||||
if (_query.maxAmount) {
|
||||
q.modify('maxAmount', _query.maxAmount);
|
||||
}
|
||||
})
|
||||
.pagination(_query.page - 1, _query.pageSize);
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ export interface ExcludedBankTransactionsQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
accountId?: number;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnexcludingEventPayload {
|
||||
|
||||
@@ -100,12 +100,14 @@ export class TriggerRecognizedTransactions {
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
tenantId,
|
||||
importId,
|
||||
meta,
|
||||
}: IImportFileCommitedEventPayload) {
|
||||
const importFile = await Import.query().findOne({ importId });
|
||||
const batch = importFile.paramsParsed.batch;
|
||||
const payload = { tenantId, transactionsCriteria: { batch } };
|
||||
|
||||
// Cannot continue if the imported resource is not bank account transactions.
|
||||
if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
|
||||
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ export class CashflowAccountTransformer extends Transformer {
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedAmount'];
|
||||
return [
|
||||
'formattedAmount',
|
||||
'lastFeedsUpdatedAt',
|
||||
'lastFeedsUpdatedAtFormatted',
|
||||
'lastFeedsUpdatedFromNow',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,7 +34,7 @@ export class CashflowAccountTransformer extends Transformer {
|
||||
|
||||
/**
|
||||
* Retrieve formatted account amount.
|
||||
* @param {IAccount} invoice
|
||||
* @param {IAccount} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (account: IAccount): string => {
|
||||
@@ -37,4 +42,22 @@ export class CashflowAccountTransformer extends Transformer {
|
||||
currencyCode: account.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the last feeds update at formatted date.
|
||||
* @param {IAccount} account
|
||||
* @returns {string}
|
||||
*/
|
||||
protected lastFeedsUpdatedAtFormatted(account: IAccount): string {
|
||||
return this.formatDate(account.lastFeedsUpdatedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last feeds updated from now.
|
||||
* @param {IAccount} account
|
||||
* @returns {string}
|
||||
*/
|
||||
protected lastFeedsUpdatedFromNow(account: IAccount): string {
|
||||
return this.formatDateFromNow(account.lastFeedsUpdatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class GetRecognizedTransactionsService {
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const _filter = {
|
||||
const _query = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
...filter,
|
||||
@@ -41,11 +41,26 @@ export class GetRecognizedTransactionsService {
|
||||
// Exclude the pending transactions.
|
||||
q.modify('notPending');
|
||||
|
||||
if (_filter.accountId) {
|
||||
q.where('accountId', _filter.accountId);
|
||||
if (_query.accountId) {
|
||||
q.where('accountId', _query.accountId);
|
||||
}
|
||||
if (_query.minDate) {
|
||||
q.modify('fromDate', _query.minDate);
|
||||
}
|
||||
if (_query.maxDate) {
|
||||
q.modify('toDate', _query.maxDate);
|
||||
}
|
||||
if (_query.minAmount) {
|
||||
q.modify('minAmount', _query.minAmount);
|
||||
}
|
||||
if (_query.maxAmount) {
|
||||
q.modify('maxAmount', _query.maxAmount);
|
||||
}
|
||||
if (_query.accountId) {
|
||||
q.where('accountId', _query.accountId);
|
||||
}
|
||||
})
|
||||
.pagination(_filter.page - 1, _filter.pageSize);
|
||||
.pagination(_query.page - 1, _query.pageSize);
|
||||
|
||||
const data = await this.transformer.transform(
|
||||
tenantId,
|
||||
|
||||
@@ -62,6 +62,19 @@ export class GetUncategorizedTransactions {
|
||||
|
||||
q.whereNull('matchedBankTransactions.id');
|
||||
q.orderBy('date', 'DESC');
|
||||
|
||||
if (_query.minDate) {
|
||||
q.modify('fromDate', _query.minDate);
|
||||
}
|
||||
if (_query.maxDate) {
|
||||
q.modify('toDate', _query.maxDate);
|
||||
}
|
||||
if (_query.minAmount) {
|
||||
q.modify('minAmount', _query.minAmount);
|
||||
}
|
||||
if (_query.maxAmount) {
|
||||
q.modify('maxAmount', _query.maxAmount);
|
||||
}
|
||||
})
|
||||
.pagination(_query.page - 1, _query.pageSize);
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the import params before storing them.
|
||||
* Transforms the import params before storing them.
|
||||
* @param {Record<string, any>} parmas
|
||||
*/
|
||||
public transformParams(parmas: Record<string, any>) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import { CustomersApplication } from './CustomersApplication';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class CustomersExportable extends Exportable {
|
||||
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
|
||||
const parsedQuery = {
|
||||
sortOrder: 'DESC',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
...query,
|
||||
pageSize: 12,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IItemsFilter;
|
||||
|
||||
return this.customersApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { VendorsApplication } from './VendorsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class VendorsExportable extends Exportable {
|
||||
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
|
||||
const parsedQuery = {
|
||||
sortOrder: 'DESC',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
...query,
|
||||
pageSize: 12,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IItemsFilter;
|
||||
|
||||
return this.vendorsApplication
|
||||
|
||||
@@ -5,11 +5,17 @@ import * as R from 'ramda';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
import { ICreditNote, ICreditNoteEditDTO, ICreditNoteNewDTO } from '@/interfaces';
|
||||
import {
|
||||
ICreditNote,
|
||||
ICreditNoteEditDTO,
|
||||
ICreditNoteEntryNewDTO,
|
||||
ICreditNoteNewDTO,
|
||||
} from '@/interfaces';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
|
||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { assocItemEntriesDefaultIndex } from '../Items/utils';
|
||||
|
||||
@Service()
|
||||
export default class BaseCreditNotes {
|
||||
@@ -43,10 +49,17 @@ export default class BaseCreditNotes {
|
||||
const amount = this.itemsEntriesService.getTotalItemsEntries(
|
||||
creditNoteDTO.entries
|
||||
);
|
||||
const entries = creditNoteDTO.entries.map((entry) => ({
|
||||
...entry,
|
||||
referenceType: 'CreditNote',
|
||||
}));
|
||||
const entries = R.compose(
|
||||
// Associate the default index to each item entry.
|
||||
assocItemEntriesDefaultIndex,
|
||||
|
||||
// Associate the reference type to credit note entries.
|
||||
R.map((entry: ICreditNoteEntryNewDTO) => ({
|
||||
...entry,
|
||||
referenceType: 'CreditNote',
|
||||
}))
|
||||
)(creditNoteDTO.entries);
|
||||
|
||||
// Retreive the next credit note number.
|
||||
const autoNextNumber = this.getNextCreditNumber(tenantId);
|
||||
|
||||
|
||||
24
packages/server/src/services/EventsTracker/PostHog.ts
Normal file
24
packages/server/src/services/EventsTracker/PostHog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { Service } from 'typedi';
|
||||
import { EventMessage } from 'posthog-node/src/types';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class PosthogService {
|
||||
public posthog: PostHog;
|
||||
|
||||
constructor() {
|
||||
if (config.posthog.apiKey && config.posthog.host) {
|
||||
this.posthog = new PostHog(config.posthog.apiKey, {
|
||||
host: config.posthog.host,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public trackEvent(event: EventMessage) {
|
||||
// Cannot continue if the Posthog not configured.
|
||||
if (!this.posthog) return;
|
||||
|
||||
this.posthog.capture(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
IAccountEventCreatedPayload,
|
||||
IAccountEventEditedPayload,
|
||||
IAccountEventDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ACCOUNT_CREATED,
|
||||
ACCOUNT_EDITED,
|
||||
ACCOUNT_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class AccountEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.accounts.onCreated,
|
||||
this.handleTrackAccountCreatedEvent
|
||||
);
|
||||
bus.subscribe(events.accounts.onEdited, this.handleTrackEditedAccountEvent);
|
||||
bus.subscribe(
|
||||
events.accounts.onDeleted,
|
||||
this.handleTrackDeletedAccountEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackAccountCreatedEvent = ({
|
||||
tenantId,
|
||||
}: IAccountEventCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ACCOUNT_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedAccountEvent = ({
|
||||
tenantId,
|
||||
}: IAccountEventEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ACCOUNT_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedAccountEvent = ({
|
||||
tenantId,
|
||||
}: IAccountEventDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ACCOUNT_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { IAuthSignedUpEventPayload } from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import { AUTH_SIGNED_UP } from '@/constants/event-tracker';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class AuthenticationEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.auth.signUp, this.handleTrackSignUpEvent);
|
||||
}
|
||||
|
||||
private handleTrackSignUpEvent = ({
|
||||
signupDTO,
|
||||
user,
|
||||
tenant,
|
||||
}: IAuthSignedUpEventPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: user.email,
|
||||
event: AUTH_SIGNED_UP,
|
||||
properties: {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
IBillPaymentEventCreatedPayload,
|
||||
IBillPaymentEventEditedPayload,
|
||||
IBillPaymentEventDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
BILL_CREATED,
|
||||
BILL_EDITED,
|
||||
BILL_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class BillEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.bill.onCreated, this.handleTrackBillCreatedEvent);
|
||||
bus.subscribe(events.bill.onEdited, this.handleTrackEditedBillEvent);
|
||||
bus.subscribe(events.bill.onDeleted, this.handleTrackDeletedBillEvent);
|
||||
}
|
||||
|
||||
private handleTrackBillCreatedEvent = ({
|
||||
tenantId,
|
||||
}: IBillPaymentEventCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: BILL_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedBillEvent = ({
|
||||
tenantId,
|
||||
}: IBillPaymentEventEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: BILL_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedBillEvent = ({
|
||||
tenantId,
|
||||
}: IBillPaymentEventDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: BILL_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
IExpenseCreatedPayload,
|
||||
IExpenseEventEditPayload,
|
||||
IExpenseEventDeletePayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
EXPENSE_CREATED,
|
||||
EXPENSE_EDITED,
|
||||
EXPENSE_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class ExpenseEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.expenses.onCreated,
|
||||
this.handleTrackExpenseCreatedEvent
|
||||
);
|
||||
bus.subscribe(events.expenses.onEdited, this.handleTrackEditedExpenseEvent);
|
||||
bus.subscribe(
|
||||
events.expenses.onDeleted,
|
||||
this.handleTrackDeletedExpenseEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackExpenseCreatedEvent = ({
|
||||
tenantId,
|
||||
}: IExpenseCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: EXPENSE_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedExpenseEvent = ({
|
||||
tenantId,
|
||||
}: IExpenseEventEditPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: EXPENSE_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedExpenseEvent = ({
|
||||
tenantId,
|
||||
}: IExpenseEventDeletePayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: EXPENSE_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
IItemEventCreatedPayload,
|
||||
IItemEventEditedPayload,
|
||||
IItemEventDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ITEM_EVENT_CREATED,
|
||||
ITEM_EVENT_EDITED,
|
||||
ITEM_EVENT_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class ItemEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
|
||||
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
|
||||
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
|
||||
}
|
||||
|
||||
private handleTrackItemCreatedEvent = ({
|
||||
tenantId,
|
||||
}: IItemEventCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ITEM_EVENT_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedItemEvent = ({
|
||||
tenantId,
|
||||
}: IItemEventEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ITEM_EVENT_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedItemEvent = ({
|
||||
tenantId,
|
||||
}: IItemEventDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ITEM_EVENT_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
IBillPaymentEventCreatedPayload,
|
||||
IBillPaymentEditingPayload,
|
||||
IBillPaymentEventDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
PAYMENT_MADE_CREATED,
|
||||
PAYMENT_MADE_EDITED,
|
||||
PAYMENT_MADE_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class PaymentMadeEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.billPayment.onCreated,
|
||||
this.handleTrackPaymentMadeCreatedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.billPayment.onEdited,
|
||||
this.handleTrackEditedPaymentMadeEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.billPayment.onDeleted,
|
||||
this.handleTrackDeletedPaymentMadeEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackPaymentMadeCreatedEvent = ({
|
||||
tenantId,
|
||||
}: IBillPaymentEventCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_MADE_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedPaymentMadeEvent = ({
|
||||
tenantId,
|
||||
}: IBillPaymentEditingPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_MADE_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedPaymentMadeEvent = ({
|
||||
tenantId,
|
||||
}: IBillPaymentEventDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_MADE_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
IPaymentReceivedCreatedPayload,
|
||||
IPaymentReceivedEditedPayload,
|
||||
IPaymentReceivedDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
PAYMENT_RECEIVED_CREATED,
|
||||
PAYMENT_RECEIVED_EDITED,
|
||||
PAYMENT_RECEIVED_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceivedEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.paymentReceive.onCreated,
|
||||
this.handleTrackPaymentReceivedCreatedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.paymentReceive.onEdited,
|
||||
this.handleTrackEditedPaymentReceivedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.paymentReceive.onDeleted,
|
||||
this.handleTrackDeletedPaymentReceivedEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackPaymentReceivedCreatedEvent = ({
|
||||
tenantId,
|
||||
}: IPaymentReceivedCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_RECEIVED_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedPaymentReceivedEvent = ({
|
||||
tenantId,
|
||||
}: IPaymentReceivedEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_RECEIVED_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedPaymentReceivedEvent = ({
|
||||
tenantId,
|
||||
}: IPaymentReceivedDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_RECEIVED_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
ISaleEstimateCreatedPayload,
|
||||
ISaleEstimateEditedPayload,
|
||||
ISaleEstimateDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
SALE_ESTIMATE_CREATED,
|
||||
SALE_ESTIMATE_EDITED,
|
||||
SALE_ESTIMATE_DELETED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimateEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleEstimate.onCreated,
|
||||
this.handleTrackEstimateCreatedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleEstimate.onEdited,
|
||||
this.handleTrackEditedEstimateEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleEstimate.onDeleted,
|
||||
this.handleTrackDeletedEstimateEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackEstimateCreatedEvent = ({
|
||||
tenantId,
|
||||
}: ISaleEstimateCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_ESTIMATE_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedEstimateEvent = ({
|
||||
tenantId,
|
||||
}: ISaleEstimateEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_ESTIMATE_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedEstimateEvent = ({
|
||||
tenantId,
|
||||
}: ISaleEstimateDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_ESTIMATE_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceEditedPayload,
|
||||
} from '@/interfaces';
|
||||
import { PosthogService } from '../PostHog';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
SALE_INVOICE_CREATED,
|
||||
SALE_INVOICE_DELETED,
|
||||
SALE_INVOICE_EDITED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceEventsTracker extends EventSubscriber {
|
||||
@Inject()
|
||||
private posthog: PosthogService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreated,
|
||||
this.handleTrackInvoiceCreatedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEdited,
|
||||
this.handleTrackEditedInvoiceEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDeleted,
|
||||
this.handleTrackDeletedInvoiceEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackInvoiceCreatedEvent = ({
|
||||
tenantId,
|
||||
}: ISaleInvoiceCreatedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_CREATED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackEditedInvoiceEvent = ({
|
||||
tenantId,
|
||||
}: ISaleInvoiceEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_EDITED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackDeletedInvoiceEvent = ({
|
||||
tenantId,
|
||||
}: ISaleInvoiceEditedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_DELETED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
21
packages/server/src/services/EventsTracker/events/events.ts
Normal file
21
packages/server/src/services/EventsTracker/events/events.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { SaleInvoiceEventsTracker } from './SaleInvoicesEventsTracker';
|
||||
import { SaleEstimateEventsTracker } from './SaleEstimateEventsTracker';
|
||||
import { PaymentMadeEventsTracker } from './PaymentMadeEventsTracker';
|
||||
import { PaymentReceivedEventsTracker } from './PaymentReceivedEventsTracker';
|
||||
import { BillEventsTracker } from './BillEventsTracker';
|
||||
import { ExpenseEventsTracker } from './ExpenseEventsTracker';
|
||||
import { AccountEventsTracker } from './AccountEventsTracker';
|
||||
import { AuthenticationEventsTracker } from './AuthenticationEventsTracker';
|
||||
import { ItemEventsTracker } from './ItemEventsTracker';
|
||||
|
||||
export const EventsTrackerListeners = [
|
||||
SaleInvoiceEventsTracker,
|
||||
SaleEstimateEventsTracker,
|
||||
PaymentMadeEventsTracker,
|
||||
PaymentReceivedEventsTracker,
|
||||
BillEventsTracker,
|
||||
AccountEventsTracker,
|
||||
ExpenseEventsTracker,
|
||||
AuthenticationEventsTracker,
|
||||
ItemEventsTracker
|
||||
];
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/interfaces';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
|
||||
@Service()
|
||||
export class ExpenseDTOTransformer {
|
||||
@@ -40,8 +41,8 @@ export class ExpenseDTOTransformer {
|
||||
|
||||
/**
|
||||
* Mapping expense DTO to model.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @return {IExpense}
|
||||
*/
|
||||
private expenseDTOToModel(
|
||||
@@ -52,9 +53,14 @@ export class ExpenseDTOTransformer {
|
||||
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
|
||||
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
|
||||
|
||||
const categories = R.compose(
|
||||
// Associate the default index to categories lines.
|
||||
assocItemEntriesDefaultIndex
|
||||
)(expenseDTO.categories || []);
|
||||
|
||||
const initialDTO = {
|
||||
categories: [],
|
||||
...omit(expenseDTO, ['publish', 'attachments']),
|
||||
categories,
|
||||
totalAmount,
|
||||
landedCostAmount,
|
||||
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { IExpensesFilter } from '@/interfaces';
|
||||
import { ExpensesApplication } from './ExpensesApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class ExpensesExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IExpensesFilter;
|
||||
|
||||
return this.expensesApplication
|
||||
|
||||
48
packages/server/src/services/Export/ExportAls.ts
Normal file
48
packages/server/src/services/Export/ExportAls.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Service } from 'typedi';
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
@Service()
|
||||
export class ExportAls {
|
||||
private als: AsyncLocalStorage<Map<string, any>>;
|
||||
|
||||
constructor() {
|
||||
this.als = new AsyncLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function within the context of a new AsyncLocalStorage store.
|
||||
* @param callback The function to be executed within the AsyncLocalStorage context.
|
||||
* @returns The result of the callback function.
|
||||
*/
|
||||
public run<T>(callback: () => T): T {
|
||||
return this.als.run<T>(new Map(), () => {
|
||||
this.markAsExport();
|
||||
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current AsyncLocalStorage store.
|
||||
* @returns The current store or undefined if not in a valid context.
|
||||
*/
|
||||
public getStore(): Map<string, any> | undefined {
|
||||
return this.als.getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an export operation.
|
||||
* @param flag Boolean flag to set or unset the export status. Defaults to true.
|
||||
*/
|
||||
public markAsExport(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isExport', flag);
|
||||
}
|
||||
/**
|
||||
* Checks if the current context is an export operation.
|
||||
* @returns {boolean} True if the context is an export operation, false otherwise.
|
||||
*/
|
||||
public get isExport(): boolean {
|
||||
return !!this.getStore()?.get('isExport');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { Errors, ExportFormat } from './common';
|
||||
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
|
||||
import { flatDataCollections, getDataAccessor } from './utils';
|
||||
import { ExportPdf } from './ExportPdf';
|
||||
import { ExportAls } from './ExportAls';
|
||||
|
||||
@Service()
|
||||
export class ExportResourceService {
|
||||
@@ -22,13 +23,33 @@ export class ExportResourceService {
|
||||
@Inject()
|
||||
private exportPdf: ExportPdf;
|
||||
|
||||
@Inject()
|
||||
private exportAls: ExportAls;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {string} resourceName
|
||||
* @param {ExportFormat} format
|
||||
* @returns
|
||||
*/
|
||||
public async export(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
format: ExportFormat = ExportFormat.Csv
|
||||
) {
|
||||
return this.exportAls.run(() =>
|
||||
this.exportAlsRun(tenantId, resourceName, format)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the given resource data through csv, xlsx or pdf.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} resourceName - Resource name.
|
||||
* @param {ExportFormat} format - File format.
|
||||
*/
|
||||
public async export(
|
||||
public async exportAlsRun(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
format: ExportFormat = ExportFormat.Csv
|
||||
|
||||
2
packages/server/src/services/Export/constants.ts
Normal file
2
packages/server/src/services/Export/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const EXPORT_SIZE_LIMIT = 9999999;
|
||||
export const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';
|
||||
105
packages/server/src/services/Import/ImportALS.ts
Normal file
105
packages/server/src/services/Import/ImportALS.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Service } from 'typedi';
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
@Service()
|
||||
export class ImportAls {
|
||||
private als: AsyncLocalStorage<Map<string, any>>;
|
||||
|
||||
constructor() {
|
||||
this.als = new AsyncLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function within the context of a new AsyncLocalStorage store.
|
||||
* @param callback The function to be executed within the AsyncLocalStorage context.
|
||||
* @returns The result of the callback function.
|
||||
*/
|
||||
public run<T>(callback: () => T): T {
|
||||
return this.als.run<T>(new Map(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function in preview mode within the AsyncLocalStorage context.
|
||||
* @param callback The function to be executed in preview mode.
|
||||
* @returns The result of the callback function.
|
||||
*/
|
||||
public runPreview<T>(callback: () => T): T {
|
||||
return this.run(() => {
|
||||
this.markAsImport();
|
||||
this.markAsImportPreview();
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function in commit mode within the AsyncLocalStorage context.
|
||||
* @param {() => T} callback - The function to be executed in commit mode.
|
||||
* @returns {T} The result of the callback function.
|
||||
*/
|
||||
public runCommit<T>(callback: () => T): T {
|
||||
return this.run(() => {
|
||||
this.markAsImport();
|
||||
this.markAsImportCommit();
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current AsyncLocalStorage store.
|
||||
* @returns The current store or undefined if not in a valid context.
|
||||
*/
|
||||
public getStore(): Map<string, any> | undefined {
|
||||
return this.als.getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an import operation.
|
||||
* @param flag Boolean flag to set or unset the import status. Defaults to true.
|
||||
*/
|
||||
public markAsImport(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isImport', flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an import commit operation.
|
||||
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
|
||||
*/
|
||||
public markAsImportCommit(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isImportCommit', flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an import preview operation.
|
||||
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
|
||||
*/
|
||||
public markAsImportPreview(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isImportPreview', flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is an import operation.
|
||||
* @returns {boolean} True if the context is an import operation, false otherwise.
|
||||
*/
|
||||
public get isImport(): boolean {
|
||||
return !!this.getStore()?.get('isImport');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is an import commit operation.
|
||||
* @returns {boolean} True if the context is an import commit operation, false otherwise.
|
||||
*/
|
||||
public get isImportCommit(): boolean {
|
||||
return !!this.getStore()?.get('isImportCommit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is an import preview operation.
|
||||
* @returns {boolean} True if the context is an import preview operation, false otherwise.
|
||||
*/
|
||||
public get isImportPreview(): boolean {
|
||||
return !!this.getStore()?.get('isImportPreview');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import XLSX from 'xlsx';
|
||||
import bluebird from 'bluebird';
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Service } from 'typedi';
|
||||
@@ -27,23 +26,7 @@ export class ImportFileCommon {
|
||||
|
||||
@Inject()
|
||||
private resource: ResourceService;
|
||||
|
||||
/**
|
||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
||||
*/
|
||||
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Imports the given parsed data to the resource storage through registered importable service.
|
||||
* @param {number} tenantId -
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ImportFileMapping {
|
||||
*/
|
||||
public async mapping(
|
||||
tenantId: number,
|
||||
importId: number,
|
||||
importId: string,
|
||||
maps: ImportMappingAttr[]
|
||||
): Promise<ImportFileMapPOJO> {
|
||||
const importFile = await Import.query()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { ImportAls } from './ImportALS';
|
||||
|
||||
@Service()
|
||||
export class ImportFilePreview {
|
||||
@@ -11,15 +12,33 @@ export class ImportFilePreview {
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private importAls: ImportAls;
|
||||
|
||||
/**
|
||||
* Preview the imported file results before commiting the transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} importId -
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(
|
||||
tenantId: number,
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
|
||||
this.previewAlsRun(tenantId, importId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the imported file results before commiting the transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(
|
||||
public async previewAlsRun(
|
||||
tenantId: number,
|
||||
importId: number
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||
|
||||
@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
|
||||
import { chain } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ERRORS,
|
||||
getSheetColumns,
|
||||
getUnmappedSheetColumns,
|
||||
readImportFile,
|
||||
} from './_utils';
|
||||
import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
|
||||
import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
import { parseSheetData } from './sheet_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcess {
|
||||
@@ -37,7 +33,7 @@ export class ImportFileProcess {
|
||||
*/
|
||||
public async import(
|
||||
tenantId: number,
|
||||
importId: number,
|
||||
importId: string,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const importFile = await Import.query()
|
||||
@@ -49,10 +45,10 @@ export class ImportFileProcess {
|
||||
if (!importFile.isMapped) {
|
||||
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
||||
}
|
||||
// Read the imported file.
|
||||
// Read the imported file and parse the given buffer to get columns
|
||||
// and sheet data in json format.
|
||||
const buffer = await readImportFile(importFile.filename);
|
||||
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||
const header = getSheetColumns(sheetData);
|
||||
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||
|
||||
const resource = importFile.resource;
|
||||
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
||||
@@ -87,7 +83,7 @@ export class ImportFileProcess {
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
||||
const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
|
||||
const totalCount = allData.length;
|
||||
|
||||
const createdCount = successedImport.length;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
import { ImportAls } from './ImportALS';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcessCommit {
|
||||
@@ -14,18 +15,36 @@ export class ImportFileProcessCommit {
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private importAls: ImportAls;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Commits the imported file under ALS.
|
||||
* @param {number} tenantId
|
||||
* @param {string} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public commit(
|
||||
tenantId: number,
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
|
||||
this.commitAlsRun(tenantId, importId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the imported file.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async commit(
|
||||
public async commitAlsRun(
|
||||
tenantId: number,
|
||||
importId: number
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||
import { ImportFileUploadPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
import { parseSheetData } from './sheet_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportFileUploadService {
|
||||
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
|
||||
const buffer = await readImportFile(filename);
|
||||
|
||||
// Parse the buffer file to array data.
|
||||
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
||||
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||
|
||||
// Throws service error if the sheet data is empty.
|
||||
validateSheetEmpty(sheetData);
|
||||
|
||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||
|
||||
try {
|
||||
// Validates the params Yup schema.
|
||||
await this.importFileCommon.validateParamsSchema(resource, params);
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ImportResourceApplication {
|
||||
*/
|
||||
public async mapping(
|
||||
tenantId: number,
|
||||
importId: number,
|
||||
importId: string,
|
||||
maps: ImportMappingAttr[]
|
||||
) {
|
||||
return this.importMappingService.mapping(tenantId, importId, maps);
|
||||
@@ -67,7 +67,7 @@ export class ImportResourceApplication {
|
||||
* @param {number} importId - Import id.
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(tenantId: number, importId: number) {
|
||||
public async preview(tenantId: number, importId: string) {
|
||||
return this.ImportFilePreviewService.preview(tenantId, importId);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export class ImportResourceApplication {
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async process(tenantId: number, importId: number) {
|
||||
public async process(tenantId: number, importId: string) {
|
||||
return this.importProcessCommit.commit(tenantId, importId);
|
||||
}
|
||||
|
||||
|
||||
56
packages/server/src/services/Import/sheet_utils.ts
Normal file
56
packages/server/src/services/Import/sheet_utils.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import XLSX from 'xlsx';
|
||||
import { first } from 'lodash';
|
||||
|
||||
/**
|
||||
* Parses the given sheet buffer to worksheet.
|
||||
* @param {Buffer} buffer
|
||||
* @returns {XLSX.WorkSheet}
|
||||
*/
|
||||
export function parseFirstSheet(buffer: Buffer): XLSX.WorkSheet {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the given worksheet to columns.
|
||||
* @param {XLSX.WorkSheet} worksheet
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function extractSheetColumns(worksheet: XLSX.WorkSheet): Array<string> {
|
||||
// By default, sheet_to_json scans the first row and uses the values as headers.
|
||||
// With the header: 1 option, the function exports an array of arrays of values.
|
||||
const sheetCells = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||
const sheetCols = first(sheetCells) as Array<string>;
|
||||
|
||||
return sheetCols.filter((col) => col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given worksheet to json values. the keys are columns labels.
|
||||
* @param {XLSX.WorkSheet} worksheet
|
||||
* @returns {Array<Record<string, string>>}
|
||||
*/
|
||||
export function parseSheetToJson(
|
||||
worksheet: XLSX.WorkSheet
|
||||
): Array<Record<string, string>> {
|
||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given sheet buffer then retrieves the sheet data and columns.
|
||||
* @param {Buffer} buffer
|
||||
*/
|
||||
export function parseSheetData(
|
||||
buffer: Buffer
|
||||
): [Array<Record<string, string>>, string[]] {
|
||||
const worksheet = parseFirstSheet(buffer);
|
||||
|
||||
const columns = extractSheetColumns(worksheet);
|
||||
const data = parseSheetToJson(worksheet);
|
||||
|
||||
return [data, columns];
|
||||
}
|
||||
@@ -139,24 +139,25 @@ export default class InventoryService {
|
||||
) {
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
const commonJobsQuery = {
|
||||
name: 'compute-item-cost',
|
||||
lastRunAt: { $exists: false },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
};
|
||||
// Cancel any `compute-item-cost` in the queue has upper starting date
|
||||
// with the same given item.
|
||||
await agenda.cancel({
|
||||
name: 'compute-item-cost',
|
||||
nextRunAt: { $ne: null },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
'data.startingDate': { $gt: startingDate },
|
||||
...commonJobsQuery,
|
||||
'data.startingDate': { $lte: startingDate },
|
||||
});
|
||||
// Retrieve any `compute-item-cost` in the queue has lower starting date
|
||||
// with the same given item.
|
||||
const dependsJobs = await agenda.jobs({
|
||||
name: 'compute-item-cost',
|
||||
nextRunAt: { $ne: null },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
'data.startingDate': { $lte: startingDate },
|
||||
...commonJobsQuery,
|
||||
'data.startingDate': { $gte: startingDate },
|
||||
});
|
||||
// If the depends jobs cleared.
|
||||
if (dependsJobs.length === 0) {
|
||||
await agenda.schedule(
|
||||
config.scheduleComputeItemCost,
|
||||
@@ -172,6 +173,13 @@ export default class InventoryService {
|
||||
events.inventory.onComputeItemCostJobScheduled,
|
||||
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
|
||||
);
|
||||
} else {
|
||||
// Re-schedule the jobs that have higher date from current moment.
|
||||
await Promise.all(
|
||||
dependsJobs.map((job) =>
|
||||
job.schedule(config.scheduleComputeItemCost).save()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,12 +43,22 @@ export class CreateItem {
|
||||
itemDTO.sellAccountId
|
||||
);
|
||||
}
|
||||
// Validate the income account id existance if the item is sellable.
|
||||
this.validators.validateIncomeAccountExistance(
|
||||
itemDTO.sellable,
|
||||
itemDTO.sellAccountId
|
||||
);
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.costAccountId
|
||||
);
|
||||
}
|
||||
// Validate the cost account id existance if the item is purchasable.
|
||||
this.validators.validateCostAccountExistance(
|
||||
itemDTO.purchasable,
|
||||
itemDTO.costAccountId
|
||||
);
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
tenantId,
|
||||
|
||||
@@ -55,6 +55,11 @@ export class EditItem {
|
||||
itemDTO.categoryId
|
||||
);
|
||||
}
|
||||
// Validate the income account id existance if the item is sellable.
|
||||
this.validators.validateIncomeAccountExistance(
|
||||
itemDTO.sellable,
|
||||
itemDTO.sellAccountId
|
||||
);
|
||||
// Validate the sell account existance on the storage.
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
@@ -62,6 +67,11 @@ export class EditItem {
|
||||
itemDTO.sellAccountId
|
||||
);
|
||||
}
|
||||
// Validate the cost account id existance if the item is purchasable.
|
||||
this.validators.validateCostAccountExistance(
|
||||
itemDTO.purchasable,
|
||||
itemDTO.costAccountId
|
||||
);
|
||||
// Validate the cost account existance on the storage.
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
|
||||
@@ -85,6 +85,42 @@ export class ItemsValidators {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates income account existance.
|
||||
* @param {number|null} sellable - Detarmines if the item sellable.
|
||||
* @param {number|null} incomeAccountId - Income account id.
|
||||
* @throws {ServiceError(ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM)}
|
||||
*/
|
||||
public validateIncomeAccountExistance(
|
||||
sellable?: boolean,
|
||||
incomeAccountId?: number
|
||||
) {
|
||||
if (sellable && !incomeAccountId) {
|
||||
throw new ServiceError(
|
||||
ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM,
|
||||
'Income account is require with sellable item.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cost account existance.
|
||||
* @param {boolean|null} purchasable - Detarmines if the item purchasble.
|
||||
* @param {number|null} costAccountId - Cost account id.
|
||||
* @throws {ServiceError(ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM)}
|
||||
*/
|
||||
public validateCostAccountExistance(
|
||||
purchasable: boolean,
|
||||
costAccountId?: number
|
||||
) {
|
||||
if (purchasable && !costAccountId) {
|
||||
throw new ServiceError(
|
||||
ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM,
|
||||
'The cost account is required with purchasable item.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item inventory account existance and type.
|
||||
* @param {number} tenantId
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ItemEntry } from '@/models';
|
||||
import { entriesAmountDiff } from 'utils';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const ERRORS = {
|
||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||
@@ -58,13 +59,14 @@ export default class ItemsEntriesService {
|
||||
*/
|
||||
public async filterInventoryEntries(
|
||||
tenantId: number,
|
||||
entries: IItemEntry[]
|
||||
entries: IItemEntry[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<IItemEntry[]> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
|
||||
// Retrieve entries inventory items.
|
||||
const inventoryItems = await Item.query()
|
||||
const inventoryItems = await Item.query(trx)
|
||||
.whereIn('id', entriesItemsIds)
|
||||
.where('type', 'inventory');
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import { ItemsApplication } from './ItemsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class ItemsExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class ItemsExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
...query,
|
||||
pageSize: 12,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IItemsFilter;
|
||||
|
||||
return this.itemsApplication
|
||||
|
||||
@@ -26,6 +26,11 @@ export const ERRORS = {
|
||||
|
||||
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
|
||||
|
||||
INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM:
|
||||
'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
|
||||
COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM:
|
||||
'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
|
||||
16
packages/server/src/services/Items/utils.ts
Normal file
16
packages/server/src/services/Items/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IItemEntry } from '@/interfaces';
|
||||
import { isNull, isUndefined } from 'lodash';
|
||||
|
||||
export function assocItemEntriesDefaultIndex<T>(
|
||||
entries: Array<T & { index?: number }>
|
||||
): Array<T & { index: number }> {
|
||||
return entries.map((entry, index) => {
|
||||
return {
|
||||
index:
|
||||
isUndefined(entry.index) || isNull(entry.index)
|
||||
? index + 1
|
||||
: entry.index,
|
||||
...entry,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandManualJournalValidators } from './CommandManualJournalValidators';
|
||||
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
|
||||
import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer';
|
||||
import { assocItemEntriesDefaultIndex } from '../Items/utils';
|
||||
|
||||
@Service()
|
||||
export class CreateManualJournalService {
|
||||
@Inject()
|
||||
@@ -58,16 +60,22 @@ export class CreateManualJournalService {
|
||||
// The manual or auto-increment journal number.
|
||||
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
|
||||
|
||||
const entries = R.compose(
|
||||
// Associate the default index to each item entry.
|
||||
assocItemEntriesDefaultIndex
|
||||
)(manualJournalDTO.entries);
|
||||
|
||||
const initialDTO = {
|
||||
...omit(manualJournalDTO, ['publish', 'attachments']),
|
||||
...(manualJournalDTO.publish
|
||||
? { publishedAt: moment().toMySqlDateTime() }
|
||||
: {}),
|
||||
amount,
|
||||
date,
|
||||
currencyCode: manualJournalDTO.currencyCode || baseCurrency,
|
||||
exchangeRate: manualJournalDTO.exchangeRate || 1,
|
||||
date,
|
||||
journalNumber,
|
||||
entries,
|
||||
userId: authorizedUser.id,
|
||||
};
|
||||
return R.compose(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IManualJournalsFilter } from '@/interfaces';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { ManualJournalsApplication } from './ManualJournalsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class ManualJournalsExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class ManualJournalsExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IManualJournalsFilter;
|
||||
|
||||
return this.manualJournalsApplication
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import uniqid from 'uniqid';
|
||||
import AuthenticationApplication from '../Authentication/AuthApplication';
|
||||
import OrganizationService from '../Organization/OrganizationService';
|
||||
import { OneClickDemo } from '@/system/models/OneclickDemo';
|
||||
import { SystemUser } from '@/system/models';
|
||||
import { IAuthSignInPOJO } from '@/interfaces';
|
||||
import { ICreateOneClickDemoPOJO } from './interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { defaultDemoOrganizationDTO } from './_constants';
|
||||
|
||||
@Service()
|
||||
export class CreateOneClickDemo {
|
||||
@Inject()
|
||||
private authApp: AuthenticationApplication;
|
||||
|
||||
@Inject()
|
||||
private organizationService: OrganizationService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
|
||||
/**
|
||||
* Creates one-click demo account.
|
||||
* @returns {Promise<ICreateOneClickDemoPOJO>}
|
||||
*/
|
||||
public async createOneClickDemo(): Promise<ICreateOneClickDemoPOJO> {
|
||||
const firstName = faker.person.firstName();
|
||||
const lastName = faker.person.lastName();
|
||||
const email = faker.internet.email();
|
||||
const password = '123123123';
|
||||
const demoId = uniqid();
|
||||
|
||||
await this.authApp.signUp({ firstName, lastName, email, password });
|
||||
|
||||
const signedIn = await this.authApp.signIn(email, password);
|
||||
const tenantId = signedIn.tenant.id;
|
||||
const userId = signedIn.user.id;
|
||||
|
||||
// Creates a new one-click demo.
|
||||
await OneClickDemo.query().insert({ key: demoId, tenantId, userId });
|
||||
|
||||
const buildJob = await this.organizationService.buildRunJob(
|
||||
tenantId,
|
||||
defaultDemoOrganizationDTO,
|
||||
signedIn.user
|
||||
);
|
||||
return { email, demoId, signedIn, buildJob };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign-in automicatlly using the demo id one creating an account finish.
|
||||
* @param {string} oneClickDemoId -
|
||||
* @returns {Promise<IAuthSignInPOJO>}
|
||||
*/
|
||||
async autoSignIn(oneClickDemoId: string): Promise<IAuthSignInPOJO> {
|
||||
const foundOneclickDemo = await OneClickDemo.query()
|
||||
.findOne('key', oneClickDemoId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const userId = foundOneclickDemo.userId;
|
||||
const user = await SystemUser.query().findById(userId);
|
||||
|
||||
const email = user.email;
|
||||
const password = '123123123';
|
||||
|
||||
const signedIn = await this.authApp.signIn(email, password);
|
||||
|
||||
return signedIn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export class SeedDemoAbstract {
|
||||
/**
|
||||
* Retrieves the seeder file mapping.
|
||||
* @returns {Array<>}
|
||||
*/
|
||||
get mapping() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retireves the seeder file import params.
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
get importParams() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoBankTransactions extends SeedDemoAbstract {
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'Date', to: 'date' },
|
||||
{ from: 'Payee', to: 'payee' },
|
||||
{ from: 'Description', to: 'description' },
|
||||
{ from: 'Reference No.', to: 'referenceNo' },
|
||||
{ from: 'Amount', to: 'amount' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `bank-transactions.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'UncategorizedCashflowTransaction';
|
||||
}
|
||||
|
||||
get importParams() {
|
||||
return {
|
||||
accountId: 1001,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoAccountCustomers extends SeedDemoAbstract {
|
||||
/**
|
||||
* Retrieves the seeder file mapping.
|
||||
*/
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'Customer Type', to: 'customerType' },
|
||||
{ from: 'First Name', to: 'firstName' },
|
||||
{ from: 'Last Name', to: 'lastName' },
|
||||
{ from: 'Display Name', to: 'displayName' },
|
||||
{ from: 'Email', to: 'email' },
|
||||
{ from: 'Work Phone Number', to: 'workPhone' },
|
||||
{ from: 'Personal Phone Number', to: 'personalPhone' },
|
||||
{ from: 'Company Name', to: 'companyName' },
|
||||
{ from: 'Website', to: 'website' },
|
||||
{ from: 'Active', to: 'active' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `customers.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'Customer';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoAccountExpenses extends SeedDemoAbstract {
|
||||
/**
|
||||
* Retrieves the seeder file mapping.
|
||||
*/
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'Payment Account', to: 'paymentAccountId' },
|
||||
{ from: 'Reference No.', to: 'referenceNo' },
|
||||
{ from: 'Payment Date', to: 'paymentDate' },
|
||||
{ from: 'Description', to: 'description' },
|
||||
{ from: 'Publish', to: 'publish' },
|
||||
{
|
||||
from: 'Expense Account',
|
||||
to: 'expenseAccountId',
|
||||
group: 'categories',
|
||||
},
|
||||
{ from: 'Amount', to: 'amount', group: 'categories' },
|
||||
{ from: 'Line Description', to: 'description', group: 'categories' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `Expenses.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'Expense';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoAccountItems extends SeedDemoAbstract {
|
||||
/**
|
||||
* Retrieves the seeder file mapping.
|
||||
*/
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'Item Type', to: 'type' },
|
||||
{ from: 'Item Name', to: 'name' },
|
||||
{ from: 'Item Code', to: 'code' },
|
||||
{ from: 'Sellable', to: 'sellable' },
|
||||
{ from: 'Purchasable', to: 'purchasable' },
|
||||
{ from: 'Sell Price', to: 'sellPrice' },
|
||||
{ from: 'Cost Price', to: 'costPrice' },
|
||||
{ from: 'Cost Account', to: 'costAccountId' },
|
||||
{ from: 'Sell Account', to: 'sellAccountId' },
|
||||
{ from: 'Inventory Account', to: 'inventoryAccountId' },
|
||||
{ from: 'Sell Description', to: 'sellDescription' },
|
||||
{
|
||||
from: 'Purchase Description',
|
||||
to: 'purchaseDescription',
|
||||
},
|
||||
{ from: 'Note', to: 'note' },
|
||||
{ from: 'Category', to: 'category' },
|
||||
{ from: 'Active', to: 'active' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `items.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'Item';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoAccountManualJournals extends SeedDemoAbstract {
|
||||
/**
|
||||
* Retrieves the seeder file mapping.
|
||||
*/
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'Date', to: 'date' },
|
||||
{ from: 'Journal No', to: 'journalNumber' },
|
||||
{ from: 'Reference No.', to: 'reference' },
|
||||
{ from: 'Description', to: 'description' },
|
||||
{ from: 'Publish', to: 'publish' },
|
||||
{ from: 'Credit', to: 'credit', group: 'entries' },
|
||||
{ from: 'Debit', to: 'debit', group: 'entries' },
|
||||
{ from: 'Account', to: 'accountId', group: 'entries' },
|
||||
{ from: 'Note', to: 'note', group: 'entries' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `manual-journals.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'ManualJournal';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoSaleInvoices extends SeedDemoAbstract {
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'Invoice Date', to: 'invoiceDate' },
|
||||
{ from: 'Due Date', to: 'dueDate' },
|
||||
{ from: 'Reference No.', to: 'referenceNo' },
|
||||
{ from: 'Invoice No.', to: 'invoiceNo' },
|
||||
{ from: 'Customer', to: 'customerId' },
|
||||
{ from: 'Exchange Rate', to: 'exchangeRate' },
|
||||
{ from: 'Invoice Message', to: 'invoiceMessage' },
|
||||
{ from: 'Terms & Conditions', to: 'termsConditions' },
|
||||
{ from: 'Delivered', to: 'delivered' },
|
||||
{ from: 'Item', to: 'itemId', group: 'entries' },
|
||||
{ from: 'Rate', to: 'rate', group: 'entries' },
|
||||
{ from: 'Quantity', to: 'quantity', group: 'entries' },
|
||||
{ from: 'Description', to: 'description', group: 'entries' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `sale-invoices.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'SaleInvoice';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { SeedDemoAbstract } from './SeedDemoAbstract';
|
||||
|
||||
export class SeedDemoAccountVendors extends SeedDemoAbstract {
|
||||
/**
|
||||
* Retrieves the seeder file mapping.
|
||||
*/
|
||||
get mapping() {
|
||||
return [
|
||||
{ from: 'First Name', to: 'firstName' },
|
||||
{ from: 'Last Name', to: 'lastName' },
|
||||
{ from: 'Display Name', to: 'displayName' },
|
||||
{ from: 'Email', to: 'email' },
|
||||
{ from: 'Work Phone Number', to: 'workPhone' },
|
||||
{ from: 'Personal Phone Number', to: 'personalPhone' },
|
||||
{ from: 'Company Name', to: 'companyName' },
|
||||
{ from: 'Website', to: 'website' },
|
||||
{ from: 'Active', to: 'active' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the seeder file name.
|
||||
* @returns {string}
|
||||
*/
|
||||
get importFileName() {
|
||||
return `vendors.csv`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource name of the seeder.
|
||||
* @returns {string}
|
||||
*/
|
||||
get resource() {
|
||||
return 'Vendor';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { CreateOneClickDemo } from './CreateOneClickDemo';
|
||||
|
||||
@Service()
|
||||
export class OneClickDemoApplication {
|
||||
@Inject()
|
||||
private createOneClickDemoService: CreateOneClickDemo;
|
||||
|
||||
/**
|
||||
* Creates one-click demo account.
|
||||
* @returns {Promise<ICreateOneClickDemoPOJO>}
|
||||
*/
|
||||
public createOneClick() {
|
||||
return this.createOneClickDemoService.createOneClickDemo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-sign-in to created demo account.
|
||||
* @param {string} demoId -
|
||||
* @returns {Promise<IAuthSignInPOJO>}
|
||||
*/
|
||||
public autoSignIn(demoId: string) {
|
||||
return this.createOneClickDemoService.autoSignIn(demoId);
|
||||
}
|
||||
}
|
||||
12
packages/server/src/services/OneClickDemo/_constants.ts
Normal file
12
packages/server/src/services/OneClickDemo/_constants.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
export const defaultDemoOrganizationDTO = {
|
||||
name: 'BIGCAPITAL, INC',
|
||||
baseCurrency: 'USD',
|
||||
location: 'US',
|
||||
language: 'en',
|
||||
industry: 'Technology',
|
||||
fiscalYear: 'march',
|
||||
timezone: 'US/Central',
|
||||
dateFormat: 'MM/DD/yyyy',
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import uniqid from 'uniqid';
|
||||
import { isEmpty } from 'lodash';
|
||||
import events from '@/subscribers/events';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { IOrganizationBuiltEventPayload } from '@/interfaces';
|
||||
import { SeedDemoAccountItems } from '../DemoSeeders/SeedDemoItems';
|
||||
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
|
||||
import { getImportsStoragePath } from '@/services/Import/_utils';
|
||||
import { OneClickDemo } from '@/system/models/OneclickDemo';
|
||||
import { SeedDemoAccountCustomers } from '../DemoSeeders/SeedDemoCustomers';
|
||||
import { SeedDemoAccountVendors } from '../DemoSeeders/SeedDemoVendors';
|
||||
import { SeedDemoAccountManualJournals } from '../DemoSeeders/SeedDemoManualJournals';
|
||||
import { SeedDemoAccountExpenses } from '../DemoSeeders/SeedDemoExpenses';
|
||||
import { SeedDemoBankTransactions } from '../DemoSeeders/SeedDemoBankTransactions';
|
||||
import { SeedDemoSaleInvoices } from '../DemoSeeders/SeedDemoSaleInvoices';
|
||||
|
||||
export class SeedInitialDemoAccountDataOnOrgBuild {
|
||||
@Inject()
|
||||
private importApp: ImportResourceApplication;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach = (bus) => {
|
||||
bus.subscribe(
|
||||
events.organization.built,
|
||||
this.seedInitialDemoAccountDataOnOrgBuild.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Demo account seeder.
|
||||
*/
|
||||
get seedDemoAccountSeeders() {
|
||||
return [
|
||||
SeedDemoAccountItems,
|
||||
SeedDemoBankTransactions,
|
||||
SeedDemoAccountCustomers,
|
||||
SeedDemoAccountVendors,
|
||||
SeedDemoAccountManualJournals,
|
||||
SeedDemoSaleInvoices,
|
||||
SeedDemoAccountExpenses,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the seeder sheet file to the import storage first.
|
||||
* @param {string} fileName -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initiateSeederFile(fileName: string) {
|
||||
const destFileName = uniqid();
|
||||
const source = path.join(global.__views_dir, `/demo-sheets`, fileName);
|
||||
const destination = path.join(getImportsStoragePath(), destFileName);
|
||||
|
||||
// Use the fs.promises.copyFile method to copy the file
|
||||
await fs.copyFile(source, destination);
|
||||
|
||||
return destFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds initial demo account data on organization build
|
||||
* @param {IOrganizationBuildEventPayload}
|
||||
*/
|
||||
async seedInitialDemoAccountDataOnOrgBuild({
|
||||
tenantId,
|
||||
}: IOrganizationBuiltEventPayload) {
|
||||
const foundDemo = await OneClickDemo.query().findOne('tenantId', tenantId);
|
||||
|
||||
// Can't continue if the found demo is not exists.
|
||||
// Means that account is not demo account.
|
||||
if (!foundDemo) {
|
||||
return null;
|
||||
}
|
||||
const results = await PromisePool.for(this.seedDemoAccountSeeders)
|
||||
.withConcurrency(1)
|
||||
.process(async (SeedDemoAccountSeeder) => {
|
||||
const seederInstance = new SeedDemoAccountSeeder();
|
||||
|
||||
// Initialize the seeder sheet file before importing.
|
||||
const importFileName = await this.initiateSeederFile(seederInstance.importFileName);
|
||||
|
||||
// Import the given seeder file.
|
||||
const importedFile = await this.importApp.import(
|
||||
tenantId,
|
||||
seederInstance.resource,
|
||||
importFileName,
|
||||
seederInstance.importParams
|
||||
);
|
||||
// Mapping the columns with resource fields.
|
||||
await this.importApp.mapping(
|
||||
tenantId,
|
||||
importedFile.import.importId,
|
||||
seederInstance.mapping
|
||||
);
|
||||
await this.importApp.preview(tenantId, importedFile.import.importId);
|
||||
|
||||
// Commit the imported file.
|
||||
await this.importApp.process(
|
||||
tenantId,
|
||||
importedFile.import.importId
|
||||
);
|
||||
});
|
||||
|
||||
if (!isEmpty(results.errors)) {
|
||||
throw results.errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/server/src/services/OneClickDemo/interfaces.ts
Normal file
8
packages/server/src/services/OneClickDemo/interfaces.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IAuthSignInPOJO } from '@/interfaces';
|
||||
|
||||
export interface ICreateOneClickDemoPOJO {
|
||||
email: string;
|
||||
demoId: string;
|
||||
signedIn: IAuthSignInPOJO;
|
||||
buildJob: any;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IOrganizationBuildDTO,
|
||||
IOrganizationBuildEventPayload,
|
||||
IOrganizationBuiltEventPayload,
|
||||
IOrganizationUpdateDTO,
|
||||
ISystemUser,
|
||||
ITenant,
|
||||
@@ -17,6 +18,8 @@ import { Tenant } from '@/system/models';
|
||||
import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
|
||||
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationService {
|
||||
@@ -62,6 +65,10 @@ export default class OrganizationService {
|
||||
// Migrated tenant.
|
||||
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
|
||||
|
||||
// Injects the given tenant IoC services.
|
||||
await initalizeTenantServices(tenantId);
|
||||
await initializeTenantSettings(tenantId);
|
||||
|
||||
// Creates a tenancy object from given tenant model.
|
||||
const tenancyContext =
|
||||
this.tenantsManager.getSeedMigrationContext(migratedTenant);
|
||||
@@ -82,6 +89,11 @@ export default class OrganizationService {
|
||||
|
||||
//
|
||||
await this.flagTenantDBBatch(tenantId);
|
||||
|
||||
// Triggers the organization built event.
|
||||
await this.eventPublisher.emitAsync(events.organization.built, {
|
||||
tenantId: tenant.id,
|
||||
} as IOrganizationBuiltEventPayload)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { BillPaymentsApplication } from './BillPaymentsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class BillPaymentExportable extends Exportable {
|
||||
@@ -13,12 +14,16 @@ export class BillPaymentExportable extends Exportable {
|
||||
* @returns
|
||||
*/
|
||||
public exportable(tenantId: number, query: any) {
|
||||
const filterQuery = (builder) => {
|
||||
builder.withGraphFetched('entries.bill');
|
||||
};
|
||||
const parsedQuery = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...query,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
filterQuery
|
||||
} as any;
|
||||
|
||||
return this.billPaymentsApplication
|
||||
|
||||
@@ -3,8 +3,8 @@ import * as R from 'ramda';
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||
import { formatDateFields } from '@/utils';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class CommandBillPaymentDTOTransformer {
|
||||
@@ -27,6 +27,12 @@ export class CommandBillPaymentDTOTransformer {
|
||||
const amount =
|
||||
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
|
||||
|
||||
// Associate the default index to each item entry.
|
||||
const entries = R.compose(
|
||||
// Associate the default index to payment entries.
|
||||
assocItemEntriesDefaultIndex
|
||||
)(billPaymentDTO.entries);
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
|
||||
'paymentDate',
|
||||
@@ -34,7 +40,7 @@ export class CommandBillPaymentDTOTransformer {
|
||||
amount,
|
||||
currencyCode: vendor.currencyCode,
|
||||
exchangeRate: billPaymentDTO.exchangeRate || 1,
|
||||
entries: billPaymentDTO.entries,
|
||||
entries,
|
||||
};
|
||||
return R.compose(
|
||||
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)
|
||||
|
||||
@@ -52,6 +52,7 @@ export class GetBillPayments {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
|
||||
dynamicList.buildQuery()(builder);
|
||||
filter?.filterQuery && filter?.filterQuery(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user