mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
Compare commits
43 Commits
v0.18.1
...
billing-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f30c86f5f | ||
|
|
6affbedef4 | ||
|
|
ba7f32c1bf | ||
|
|
07c57ed539 | ||
|
|
788150f80d | ||
|
|
c09384e49b | ||
|
|
e11f1a95f6 | ||
|
|
b91273eee4 | ||
|
|
b5d570417b | ||
|
|
acd3265e35 | ||
|
|
894c899847 | ||
|
|
f6d4ec504f | ||
|
|
1a01461f5d | ||
|
|
89552d7ee2 | ||
|
|
4345623ea9 | ||
|
|
f457759e39 | ||
|
|
14d5e82b4a | ||
|
|
333b6f5a4b | ||
|
|
1660df20af | ||
|
|
14a9c4ba28 | ||
|
|
383be111fa | ||
|
|
7720b1cc34 | ||
|
|
db634cbb79 | ||
|
|
53f37f4f48 | ||
|
|
0a7b522b87 | ||
|
|
9e6500ac79 | ||
|
|
b93cb546f4 | ||
|
|
6d17f9cbeb | ||
|
|
998e6de211 | ||
|
|
51471ed000 | ||
|
|
fe214b1b2d | ||
|
|
6b6b73b77c | ||
|
|
c2815afbe3 | ||
|
|
fa7e6b1fca | ||
|
|
107a6f793b | ||
|
|
67d155759e | ||
|
|
7e2e87256f | ||
|
|
df7790d7c1 | ||
|
|
72128a72c4 | ||
|
|
eb3f23554f | ||
|
|
69ddf43b3e | ||
|
|
249eadaeaa | ||
|
|
59168bc691 |
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
All notable changes to Bigcapital server-side will be in this file.
|
||||||
|
|
||||||
|
## [v0.18.0] - 10-08-2024
|
||||||
|
|
||||||
|
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||||
|
* feat: Categorize & match bank transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||||
|
* feat: Reconcile match transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/522
|
||||||
|
* fix: Issues in matching transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/523
|
||||||
|
* fix: Cashflow transactions types by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/524
|
||||||
|
|
||||||
## [v0.17.5] - 17-06-2024
|
## [v0.17.5] - 17-06-2024
|
||||||
|
|
||||||
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ import { NextFunction, Request, Response, Router } from 'express';
|
|||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
||||||
|
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankAccountsController extends BaseController {
|
export class BankAccountsController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private getBankAccountSummaryService: GetBankAccountSummary;
|
private getBankAccountSummaryService: GetBankAccountSummary;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private bankAccountsApp: BankAccountsApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -16,6 +20,11 @@ export class BankAccountsController extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
||||||
|
router.post(
|
||||||
|
'/:bankAccountId/disconnect',
|
||||||
|
this.disconnectBankAccount.bind(this)
|
||||||
|
);
|
||||||
|
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
@@ -46,4 +55,58 @@ export class BankAccountsController extends BaseController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disonnect the given bank account.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response|null>}
|
||||||
|
*/
|
||||||
|
async disconnectBankAccount(
|
||||||
|
req: Request<{ bankAccountId: number }>,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { bankAccountId } = req.params;
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.bankAccountsApp.disconnectBankAccount(tenantId, bankAccountId);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
id: bankAccountId,
|
||||||
|
message: 'The bank account has been disconnected.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the given bank account.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response|null>}
|
||||||
|
*/
|
||||||
|
async refreshBankAccount(
|
||||||
|
req: Request<{ bankAccountId: number }>,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { bankAccountId } = req.params;
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.bankAccountsApp.refreshBankAccount(tenantId, bankAccountId);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
id: bankAccountId,
|
||||||
|
message: 'The bank account has been disconnected.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { param } from 'express-validator';
|
import { body, param, query } from 'express-validator';
|
||||||
import { NextFunction, Request, Response, Router, query } from 'express';
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication';
|
import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication';
|
||||||
|
import { map, parseInt, trim } from 'lodash';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransactionsController extends BaseController {
|
export class ExcludeBankTransactionsController extends BaseController {
|
||||||
@@ -15,9 +16,21 @@ export class ExcludeBankTransactionsController extends BaseController {
|
|||||||
public router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/transactions/exclude',
|
||||||
|
[body('ids').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.excludeBulkBankTransactions.bind(this)
|
||||||
|
);
|
||||||
|
router.put(
|
||||||
|
'/transactions/unexclude',
|
||||||
|
[body('ids').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.unexcludeBulkBankTransactins.bind(this)
|
||||||
|
);
|
||||||
router.put(
|
router.put(
|
||||||
'/transactions/:transactionId/exclude',
|
'/transactions/:transactionId/exclude',
|
||||||
[param('transactionId').exists()],
|
[param('transactionId').exists().toInt()],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.excludeBankTransaction.bind(this)
|
this.excludeBankTransaction.bind(this)
|
||||||
);
|
);
|
||||||
@@ -94,6 +107,63 @@ export class ExcludeBankTransactionsController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude bank transactions in bulk.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async excludeBulkBankTransactions(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { ids } = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.excludeBankTransactionApp.excludeBankTransactions(
|
||||||
|
tenantId,
|
||||||
|
ids
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The given bank transactions have been excluded',
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unexclude the given bank transactions in bulk.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response | null>}
|
||||||
|
*/
|
||||||
|
private async unexcludeBulkBankTransactins(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<Response | null> {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { ids } = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.excludeBankTransactionApp.unexcludeBankTransactions(
|
||||||
|
tenantId,
|
||||||
|
ids
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The given bank transactions have been excluded',
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the excluded uncategorized bank transactions.
|
* Retrieves the excluded uncategorized bank transactions.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
@@ -109,7 +179,6 @@ export class ExcludeBankTransactionsController extends BaseController {
|
|||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const filter = this.matchedBodyData(req);
|
const filter = this.matchedBodyData(req);
|
||||||
|
|
||||||
console.log('123');
|
|
||||||
try {
|
try {
|
||||||
const data =
|
const data =
|
||||||
await this.excludeBankTransactionApp.getExcludedBankTransactions(
|
await this.excludeBankTransactionApp.getExcludedBankTransactions(
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export default class BillsPayments extends BaseController {
|
|||||||
check('vendor_id').exists().isNumeric().toInt(),
|
check('vendor_id').exists().isNumeric().toInt(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
|
check('amount').exists().isNumeric().toFloat(),
|
||||||
check('payment_account_id').exists().isNumeric().toInt(),
|
check('payment_account_id').exists().isNumeric().toInt(),
|
||||||
check('payment_number').optional({ nullable: true }).trim().escape(),
|
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
@@ -118,7 +119,7 @@ export default class BillsPayments extends BaseController {
|
|||||||
check('reference').optional().trim().escape(),
|
check('reference').optional().trim().escape(),
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').exists().isArray({ min: 1 }),
|
check('entries').exists().isArray(),
|
||||||
check('entries.*.index').optional().isNumeric().toInt(),
|
check('entries.*.index').optional().isNumeric().toInt(),
|
||||||
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
||||||
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
check('customer_id').exists().isNumeric().toInt(),
|
check('customer_id').exists().isNumeric().toInt(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
|
check('amount').exists().isNumeric().toFloat(),
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
check('reference_no').optional(),
|
check('reference_no').optional(),
|
||||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||||
@@ -158,8 +159,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').isArray({ min: 1 }),
|
check('entries').isArray({}),
|
||||||
|
|
||||||
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.index').optional().isNumeric().toInt(),
|
check('entries.*.index').optional().isNumeric().toInt(),
|
||||||
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
|||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
|
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
|
||||||
|
import { SubscriptionApplication } from '@/services/Subscription/SubscriptionApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SubscriptionController extends BaseController {
|
export class SubscriptionController extends BaseController {
|
||||||
@@ -17,6 +18,9 @@ export class SubscriptionController extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private lemonSqueezyService: LemonSqueezyService;
|
private lemonSqueezyService: LemonSqueezyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private subscriptionApp: SubscriptionApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -33,6 +37,14 @@ export class SubscriptionController extends BaseController {
|
|||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.getCheckoutUrl.bind(this)
|
this.getCheckoutUrl.bind(this)
|
||||||
);
|
);
|
||||||
|
router.post('/cancel', asyncMiddleware(this.cancelSubscription.bind(this)));
|
||||||
|
router.post('/resume', asyncMiddleware(this.resumeSubscription.bind(this)));
|
||||||
|
router.post(
|
||||||
|
'/change',
|
||||||
|
[body('variant_id').exists().trim()],
|
||||||
|
this.validationResult,
|
||||||
|
asyncMiddleware(this.changeSubscriptionPlan.bind(this))
|
||||||
|
);
|
||||||
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
|
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
@@ -85,4 +97,84 @@ export class SubscriptionController extends BaseController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the subscription of the current organization.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response|null>}
|
||||||
|
*/
|
||||||
|
private async cancelSubscription(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
status: 200,
|
||||||
|
message: 'The organization subscription has been canceled.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumes the subscription of the current organization.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response | null>}
|
||||||
|
*/
|
||||||
|
private async resumeSubscription(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.subscriptionApp.resumeSubscription(tenantId);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
status: 200,
|
||||||
|
message: 'The organization subscription has been resumed.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the main subscription plan of the current organization.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response | null>}
|
||||||
|
*/
|
||||||
|
public async changeSubscriptionPlan(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const body = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.subscriptionApp.changeSubscriptionPlan(
|
||||||
|
tenantId,
|
||||||
|
body.variantId
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The subscription plan has been changed.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,5 +236,13 @@ module.exports = {
|
|||||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
||||||
|
forcePathStyle: parseBoolean(
|
||||||
|
defaultTo(process.env.S3_FORCE_PATH_STYLE, false),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
loops: {
|
||||||
|
apiKey: process.env.LOOPS_API_KEY,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('accounts', (table) => {
|
||||||
|
table.string('plaid_item_id').nullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('accounts', (table) => {
|
||||||
|
table.dropColumn('plaid_item_id');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.table('accounts', (table) => {
|
||||||
|
table
|
||||||
|
.boolean('is_syncing_owner')
|
||||||
|
.defaultTo(false)
|
||||||
|
.after('is_feeds_active');
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return knex('accounts')
|
||||||
|
.whereNotNull('plaid_item_id')
|
||||||
|
.orWhereNotNull('plaid_account_id')
|
||||||
|
.update('is_syncing_owner', true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
table.dropColumn('is_syncing_owner');
|
||||||
|
};
|
||||||
@@ -12,8 +12,7 @@ export default class SeedAccounts extends TenantSeeder {
|
|||||||
description: this.i18n.__(account.description),
|
description: this.i18n.__(account.description),
|
||||||
currencyCode: this.tenant.metadata.baseCurrency,
|
currencyCode: this.tenant.metadata.baseCurrency,
|
||||||
seededAt: new Date(),
|
seededAt: new Date(),
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
return knex('accounts').then(async () => {
|
return knex('accounts').then(async () => {
|
||||||
// Inserts seed entries.
|
// Inserts seed entries.
|
||||||
return knex('accounts').insert(data);
|
return knex('accounts').insert(data);
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ export const TaxPayableAccount = {
|
|||||||
predefined: 1,
|
predefined: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UnearnedRevenueAccount = {
|
||||||
|
name: 'Unearned Revenue',
|
||||||
|
slug: 'unearned-revenue',
|
||||||
|
account_type: 'other-current-liability',
|
||||||
|
parent_account_id: null,
|
||||||
|
code: '50005',
|
||||||
|
active: true,
|
||||||
|
index: 1,
|
||||||
|
predefined: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PrepardExpenses = {
|
||||||
|
name: 'Prepaid Expenses',
|
||||||
|
slug: 'prepaid-expenses',
|
||||||
|
account_type: 'other-current-asset',
|
||||||
|
parent_account_id: null,
|
||||||
|
code: '100010',
|
||||||
|
active: true,
|
||||||
|
index: 1,
|
||||||
|
predefined: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
name: 'Bank Account',
|
name: 'Bank Account',
|
||||||
@@ -323,4 +345,6 @@ export default [
|
|||||||
index: 1,
|
index: 1,
|
||||||
predefined: 0,
|
predefined: 0,
|
||||||
},
|
},
|
||||||
|
UnearnedRevenueAccount,
|
||||||
|
PrepardExpenses,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface IAccountDTO {
|
|||||||
export interface IAccountCreateDTO extends IAccountDTO {
|
export interface IAccountCreateDTO extends IAccountDTO {
|
||||||
currencyCode?: string;
|
currencyCode?: string;
|
||||||
plaidAccountId?: string;
|
plaidAccountId?: string;
|
||||||
|
plaidItemId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountEditDTO extends IAccountDTO {}
|
export interface IAccountEditDTO extends IAccountDTO {}
|
||||||
@@ -37,6 +38,8 @@ export interface IAccount {
|
|||||||
accountNormal: string;
|
accountNormal: string;
|
||||||
accountParentType: string;
|
accountParentType: string;
|
||||||
bankBalance: string;
|
bankBalance: string;
|
||||||
|
plaidItemId: number | null
|
||||||
|
lastFeedsUpdatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AccountNormal {
|
export enum AccountNormal {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
IFinancialSheetCommonMeta,
|
IFinancialSheetCommonMeta,
|
||||||
INumberFormatQuery,
|
INumberFormatQuery,
|
||||||
@@ -257,7 +258,6 @@ export interface IUncategorizedCashflowTransaction {
|
|||||||
categorized: boolean;
|
categorized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CreateUncategorizedTransactionDTO {
|
export interface CreateUncategorizedTransactionDTO {
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
@@ -269,3 +269,16 @@ export interface CreateUncategorizedTransactionDTO {
|
|||||||
plaidTransactionId?: string | null;
|
plaidTransactionId?: string | null;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUncategorizedTransactionCreatingEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUncategorizedTransactionCreatedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransaction: any;
|
||||||
|
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export interface ILedgerEntry {
|
|||||||
date: Date | string;
|
date: Date | string;
|
||||||
|
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
transactionSubType: string;
|
transactionSubType?: string;
|
||||||
|
|
||||||
transactionId: number;
|
transactionId: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,12 @@
|
|||||||
import { forEach } from 'lodash';
|
|
||||||
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
|
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
|
||||||
import { createPlaidApiEvent } from './PlaidApiEventsDBSync';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
const OPTIONS = { clientApp: 'Plaid-Pattern' };
|
|
||||||
|
|
||||||
// We want to log requests to / responses from the Plaid API (via the Plaid client), as this data
|
|
||||||
// can be useful for troubleshooting.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logging function for Plaid client methods that use an access_token as an argument. Associates
|
|
||||||
* the Plaid API event log entry with the item and user the request is for.
|
|
||||||
*
|
|
||||||
* @param {string} clientMethod the name of the Plaid client method called.
|
|
||||||
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
|
|
||||||
* @param {Object} response the response from the Plaid client.
|
|
||||||
*/
|
|
||||||
const defaultLogger = async (clientMethod, clientMethodArgs, response) => {
|
|
||||||
const accessToken = clientMethodArgs[0].access_token;
|
|
||||||
// const { id: itemId, user_id: userId } = await retrieveItemByPlaidAccessToken(
|
|
||||||
// accessToken
|
|
||||||
// );
|
|
||||||
// await createPlaidApiEvent(1, 1, clientMethod, clientMethodArgs, response);
|
|
||||||
|
|
||||||
// console.log(response);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logging function for Plaid client methods that do not use access_token as an argument. These
|
|
||||||
* Plaid API event log entries will not be associated with an item or user.
|
|
||||||
*
|
|
||||||
* @param {string} clientMethod the name of the Plaid client method called.
|
|
||||||
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
|
|
||||||
* @param {Object} response the response from the Plaid client.
|
|
||||||
*/
|
|
||||||
const noAccessTokenLogger = async (
|
|
||||||
clientMethod,
|
|
||||||
clientMethodArgs,
|
|
||||||
response
|
|
||||||
) => {
|
|
||||||
// console.log(response);
|
|
||||||
|
|
||||||
// await createPlaidApiEvent(
|
|
||||||
// undefined,
|
|
||||||
// undefined,
|
|
||||||
// clientMethod,
|
|
||||||
// clientMethodArgs,
|
|
||||||
// response
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
// Plaid client methods used in this app, mapped to their appropriate logging functions.
|
|
||||||
const clientMethodLoggingFns = {
|
|
||||||
accountsGet: defaultLogger,
|
|
||||||
institutionsGet: noAccessTokenLogger,
|
|
||||||
institutionsGetById: noAccessTokenLogger,
|
|
||||||
itemPublicTokenExchange: noAccessTokenLogger,
|
|
||||||
itemRemove: defaultLogger,
|
|
||||||
linkTokenCreate: noAccessTokenLogger,
|
|
||||||
transactionsSync: defaultLogger,
|
|
||||||
sandboxItemResetLogin: defaultLogger,
|
|
||||||
};
|
|
||||||
// Wrapper for the Plaid client. This allows us to easily log data for all Plaid client requests.
|
// Wrapper for the Plaid client. This allows us to easily log data for all Plaid client requests.
|
||||||
export class PlaidClientWrapper {
|
export class PlaidClientWrapper {
|
||||||
constructor() {
|
private static instance: PlaidClientWrapper;
|
||||||
|
private client: PlaidApi;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
// Initialize the Plaid client.
|
// Initialize the Plaid client.
|
||||||
const configuration = new Configuration({
|
const configuration = new Configuration({
|
||||||
basePath: PlaidEnvironments[config.plaid.env],
|
basePath: PlaidEnvironments[config.plaid.env],
|
||||||
@@ -75,26 +18,13 @@ export class PlaidClientWrapper {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client = new PlaidApi(configuration);
|
this.client = new PlaidApi(configuration);
|
||||||
|
|
||||||
// Wrap the Plaid client methods to add a logging function.
|
|
||||||
forEach(clientMethodLoggingFns, (logFn, method) => {
|
|
||||||
this[method] = this.createWrappedClientMethod(method, logFn);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows us to log API request data for troubleshooting purposes.
|
public static getClient(): PlaidApi {
|
||||||
createWrappedClientMethod(clientMethod, log) {
|
if (!PlaidClientWrapper.instance) {
|
||||||
return async (...args) => {
|
PlaidClientWrapper.instance = new PlaidClientWrapper();
|
||||||
try {
|
}
|
||||||
const res = await this.client[clientMethod](...args);
|
return PlaidClientWrapper.instance.client;
|
||||||
await log(clientMethod, args, res);
|
|
||||||
return res;
|
|
||||||
} catch (err) {
|
|
||||||
await log(clientMethod, args, err?.response?.data);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export const s3 = new S3Client({
|
|||||||
secretAccessKey: config.s3.secretAccessKey,
|
secretAccessKey: config.s3.secretAccessKey,
|
||||||
},
|
},
|
||||||
endpoint: config.s3.endpoint,
|
endpoint: config.s3.endpoint,
|
||||||
|
forcePathStyle: config.s3.forcePathStyle,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/
|
|||||||
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
||||||
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
||||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||||
|
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
||||||
|
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -274,5 +276,9 @@ export const susbcribers = () => {
|
|||||||
|
|
||||||
// Plaid
|
// Plaid
|
||||||
RecognizeSyncedBankTranasctions,
|
RecognizeSyncedBankTranasctions,
|
||||||
|
DisconnectPlaidItemOnAccountDeleted,
|
||||||
|
|
||||||
|
// Loops
|
||||||
|
LoopsEventsSubscriber
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export default class Account extends mixin(TenantModel, [
|
|||||||
const ExpenseEntry = require('models/ExpenseCategory');
|
const ExpenseEntry = require('models/ExpenseCategory');
|
||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
||||||
|
const PlaidItem = require('models/PlaidItem');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@@ -321,6 +322,18 @@ export default class Account extends mixin(TenantModel, [
|
|||||||
query.where('categorized', false);
|
query.where('categorized', false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account model may belongs to a Plaid item.
|
||||||
|
*/
|
||||||
|
plaidItem: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: PlaidItem.default,
|
||||||
|
join: {
|
||||||
|
from: 'accounts.plaidItemId',
|
||||||
|
to: 'plaid_items.plaidItemId',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
return notFoundBillsIds;
|
return notFoundBillsIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
static changePaymentAmount(billId, amount) {
|
static changePaymentAmount(billId, amount, trx) {
|
||||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||||
return this.query()
|
return this.query(trx)
|
||||||
.where('id', billId)
|
.where('id', billId)
|
||||||
[changeMethod]('payment_amount', Math.abs(amount));
|
[changeMethod]('payment_amount', Math.abs(amount));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { Account } from 'models';
|
|||||||
import TenantRepository from '@/repositories/TenantRepository';
|
import TenantRepository from '@/repositories/TenantRepository';
|
||||||
import { IAccount } from '@/interfaces';
|
import { IAccount } from '@/interfaces';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
|
import {
|
||||||
|
PrepardExpenses,
|
||||||
|
TaxPayableAccount,
|
||||||
|
UnearnedRevenueAccount,
|
||||||
|
} from '@/database/seeds/data/accounts';
|
||||||
|
import { TenantMetadata } from '@/system/models';
|
||||||
|
|
||||||
export default class AccountRepository extends TenantRepository {
|
export default class AccountRepository extends TenantRepository {
|
||||||
/**
|
/**
|
||||||
@@ -179,4 +184,67 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds or creates the unearned revenue.
|
||||||
|
* @param {Record<string, string>} extraAttrs
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async findOrCreateUnearnedRevenue(
|
||||||
|
extraAttrs: Record<string, string> = {},
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
// Retrieves the given tenant metadata.
|
||||||
|
const tenantMeta = await TenantMetadata.query().findOne({
|
||||||
|
tenantId: this.tenantId,
|
||||||
|
});
|
||||||
|
const _extraAttrs = {
|
||||||
|
currencyCode: tenantMeta.baseCurrency,
|
||||||
|
...extraAttrs,
|
||||||
|
};
|
||||||
|
let result = await this.model
|
||||||
|
.query(trx)
|
||||||
|
.findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs });
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
result = await this.model.query(trx).insertAndFetch({
|
||||||
|
...UnearnedRevenueAccount,
|
||||||
|
..._extraAttrs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds or creates the prepard expenses account.
|
||||||
|
* @param {Record<string, string>} extraAttrs
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async findOrCreatePrepardExpenses(
|
||||||
|
extraAttrs: Record<string, string> = {},
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
// Retrieves the given tenant metadata.
|
||||||
|
const tenantMeta = await TenantMetadata.query().findOne({
|
||||||
|
tenantId: this.tenantId,
|
||||||
|
});
|
||||||
|
const _extraAttrs = {
|
||||||
|
currencyCode: tenantMeta.baseCurrency,
|
||||||
|
...extraAttrs,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await this.model
|
||||||
|
.query(trx)
|
||||||
|
.findOne({ slug: PrepardExpenses.slug, ..._extraAttrs });
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
result = await this.model.query(trx).insertAndFetch({
|
||||||
|
...PrepardExpenses,
|
||||||
|
..._extraAttrs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import CachableRepository from './CachableRepository';
|
|||||||
|
|
||||||
export default class TenantRepository extends CachableRepository {
|
export default class TenantRepository extends CachableRepository {
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
|
tenantId: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
*/
|
*/
|
||||||
constructor(knex, cache, i18n) {
|
constructor(knex, cache, i18n) {
|
||||||
super(knex, cache, i18n);
|
super(knex, cache, i18n);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
setTenantId(tenantId: number) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return ['formattedAmount', 'flattenName', 'bankBalanceFormatted'];
|
return [
|
||||||
|
'formattedAmount',
|
||||||
|
'flattenName',
|
||||||
|
'bankBalanceFormatted',
|
||||||
|
'lastFeedsUpdatedAtFormatted',
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +57,15 @@ export class AccountTransformer extends Transformer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted last feeds update at.
|
||||||
|
* @param {IAccount} account
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected lastFeedsUpdatedAtFormatted = (account: IAccount): string => {
|
||||||
|
return this.formatDate(account.lastFeedsUpdatedAt);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the accounts collection to flat or nested array.
|
* Transformes the accounts collection to flat or nested array.
|
||||||
* @param {IAccount[]}
|
* @param {IAccount[]}
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ export class CreateAccount {
|
|||||||
...createAccountDTO,
|
...createAccountDTO,
|
||||||
slug: kebabCase(createAccountDTO.name),
|
slug: kebabCase(createAccountDTO.name),
|
||||||
currencyCode: createAccountDTO.currencyCode || baseCurrency,
|
currencyCode: createAccountDTO.currencyCode || baseCurrency,
|
||||||
|
|
||||||
|
// Mark the account is Plaid owner since Plaid item/account is defined on creating.
|
||||||
|
isSyncingOwner: Boolean(
|
||||||
|
createAccountDTO.plaidAccountId || createAccountDTO.plaidItemId
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,12 +122,7 @@ export class CreateAccount {
|
|||||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
// Authorize the account creation.
|
// Authorize the account creation.
|
||||||
await this.authorize(
|
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency, params);
|
||||||
tenantId,
|
|
||||||
accountDTO,
|
|
||||||
tenantMeta.baseCurrency,
|
|
||||||
params
|
|
||||||
);
|
|
||||||
// Transformes the DTO to model.
|
// Transformes the DTO to model.
|
||||||
const accountInputModel = this.transformDTOToModel(
|
const accountInputModel = this.transformDTOToModel(
|
||||||
accountDTO,
|
accountDTO,
|
||||||
@@ -157,4 +157,3 @@ export class CreateAccount {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { DisconnectBankAccount } from './DisconnectBankAccount';
|
||||||
|
import { RefreshBankAccountService } from './RefreshBankAccount';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BankAccountsApplication {
|
||||||
|
@Inject()
|
||||||
|
private disconnectBankAccountService: DisconnectBankAccount;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private refreshBankAccountService: RefreshBankAccountService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects the given bank account.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} bankAccountId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async disconnectBankAccount(tenantId: number, bankAccountId: number) {
|
||||||
|
return this.disconnectBankAccountService.disconnectBankAccount(
|
||||||
|
tenantId,
|
||||||
|
bankAccountId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the bank transactions of the given bank account.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} bankAccountId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async refreshBankAccount(tenantId: number, bankAccountId: number) {
|
||||||
|
return this.refreshBankAccountService.refreshBankAccount(
|
||||||
|
tenantId,
|
||||||
|
bankAccountId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import { PlaidClientWrapper } from '@/lib/Plaid';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
ERRORS,
|
||||||
|
IBankAccountDisconnectedEventPayload,
|
||||||
|
IBankAccountDisconnectingEventPayload,
|
||||||
|
} from './types';
|
||||||
|
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DisconnectBankAccount {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects the given bank account.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} bankAccountId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async disconnectBankAccount(tenantId: number, bankAccountId: number) {
|
||||||
|
const { Account, PlaidItem } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Retrieve the bank account or throw not found error.
|
||||||
|
const account = await Account.query()
|
||||||
|
.findById(bankAccountId)
|
||||||
|
.whereIn('account_type', [ACCOUNT_TYPE.CASH, ACCOUNT_TYPE.BANK])
|
||||||
|
.withGraphFetched('plaidItem')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const oldPlaidItem = account.plaidItem;
|
||||||
|
|
||||||
|
if (!oldPlaidItem) {
|
||||||
|
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
|
||||||
|
}
|
||||||
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
|
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onBankAccountDisconnecting` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnecting, {
|
||||||
|
tenantId,
|
||||||
|
bankAccountId,
|
||||||
|
} as IBankAccountDisconnectingEventPayload);
|
||||||
|
|
||||||
|
// Remove the Plaid item from the system.
|
||||||
|
await PlaidItem.query(trx).findById(account.plaidItemId).delete();
|
||||||
|
|
||||||
|
// Remove the plaid item association to the bank account.
|
||||||
|
await Account.query(trx).findById(bankAccountId).patch({
|
||||||
|
plaidAccountId: null,
|
||||||
|
plaidItemId: null,
|
||||||
|
isFeedsActive: false,
|
||||||
|
});
|
||||||
|
// Remove the Plaid item.
|
||||||
|
await plaidInstance.itemRemove({
|
||||||
|
access_token: oldPlaidItem.plaidAccessToken,
|
||||||
|
});
|
||||||
|
// Triggers `onBankAccountDisconnected` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnected, {
|
||||||
|
tenantId,
|
||||||
|
bankAccountId,
|
||||||
|
trx,
|
||||||
|
} as IBankAccountDisconnectedEventPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { PlaidClientWrapper } from '@/lib/Plaid';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { ERRORS } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class RefreshBankAccountService {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks Plaid to trigger syncing the given bank account.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} bankAccountId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async refreshBankAccount(tenantId: number, bankAccountId: number) {
|
||||||
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const bankAccount = await Account.query()
|
||||||
|
.findById(bankAccountId)
|
||||||
|
.withGraphFetched('plaidItem')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Can't continue if the given account is not linked with Plaid item.
|
||||||
|
if (!bankAccount.plaidItem) {
|
||||||
|
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
|
||||||
|
}
|
||||||
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
|
|
||||||
|
await plaidInstance.transactionsRefresh({
|
||||||
|
access_token: bankAccount.plaidItem.plaidAccessToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { IAccountEventDeletedPayload } from '@/interfaces';
|
||||||
|
import { PlaidClientWrapper } from '@/lib/Plaid';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DisconnectPlaidItemOnAccountDeleted {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.accounts.onDeleted,
|
||||||
|
this.handleDisconnectPlaidItemOnAccountDelete.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes Plaid item from the system and Plaid once the account deleted.
|
||||||
|
* @param {IAccountEventDeletedPayload} payload
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
private async handleDisconnectPlaidItemOnAccountDelete({
|
||||||
|
tenantId,
|
||||||
|
oldAccount,
|
||||||
|
trx,
|
||||||
|
}: IAccountEventDeletedPayload) {
|
||||||
|
const { PlaidItem, Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Can't continue if the deleted account is not linked to Plaid item.
|
||||||
|
if (!oldAccount.plaidItemId) return;
|
||||||
|
|
||||||
|
// Retrieves the Plaid item that associated to the deleted account.
|
||||||
|
const oldPlaidItem = await PlaidItem.query(trx).findOne(
|
||||||
|
'plaidItemId',
|
||||||
|
oldAccount.plaidItemId
|
||||||
|
);
|
||||||
|
// Unlink the Plaid item from all account before deleting it.
|
||||||
|
await Account.query(trx)
|
||||||
|
.where('plaidItemId', oldAccount.plaidItemId)
|
||||||
|
.patch({
|
||||||
|
plaidAccountId: null,
|
||||||
|
plaidItemId: null,
|
||||||
|
});
|
||||||
|
// Remove the Plaid item from the system.
|
||||||
|
await PlaidItem.query(trx)
|
||||||
|
.findOne('plaidItemId', oldAccount.plaidItemId)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
if (oldPlaidItem) {
|
||||||
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
|
|
||||||
|
// Remove the Plaid item.
|
||||||
|
await plaidInstance.itemRemove({
|
||||||
|
access_token: oldPlaidItem.plaidAccessToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/server/src/services/Banking/BankAccounts/types.ts
Normal file
17
packages/server/src/services/Banking/BankAccounts/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export interface IBankAccountDisconnectingEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
bankAccountId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBankAccountDisconnectedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
bankAccountId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERRORS = {
|
||||||
|
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import PromisePool from '@supercharge/promise-pool';
|
||||||
|
import { castArray } from 'lodash';
|
||||||
|
import { ExcludeBankTransaction } from './ExcludeBankTransaction';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ExcludeBankTransactions {
|
||||||
|
@Inject()
|
||||||
|
private excludeBankTransaction: ExcludeBankTransaction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude bank transactions in bulk.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} bankTransactionIds
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async excludeBankTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
bankTransactionIds: Array<number> | number
|
||||||
|
) {
|
||||||
|
const _bankTransactionIds = castArray(bankTransactionIds);
|
||||||
|
|
||||||
|
await PromisePool.withConcurrency(1)
|
||||||
|
.for(_bankTransactionIds)
|
||||||
|
.process((bankTransactionId: number) => {
|
||||||
|
return this.excludeBankTransaction.excludeBankTransaction(
|
||||||
|
tenantId,
|
||||||
|
bankTransactionId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import { ExcludeBankTransaction } from './ExcludeBankTransaction';
|
|||||||
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
||||||
import { GetExcludedBankTransactionsService } from './GetExcludedBankTransactions';
|
import { GetExcludedBankTransactionsService } from './GetExcludedBankTransactions';
|
||||||
import { ExcludedBankTransactionsQuery } from './_types';
|
import { ExcludedBankTransactionsQuery } from './_types';
|
||||||
|
import { UnexcludeBankTransactions } from './UnexcludeBankTransactions';
|
||||||
|
import { ExcludeBankTransactions } from './ExcludeBankTransactions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransactionsApplication {
|
export class ExcludeBankTransactionsApplication {
|
||||||
@@ -15,6 +17,12 @@ export class ExcludeBankTransactionsApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getExcludedBankTransactionsService: GetExcludedBankTransactionsService;
|
private getExcludedBankTransactionsService: GetExcludedBankTransactionsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private excludeBankTransactionsService: ExcludeBankTransactions;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private unexcludeBankTransactionsService: UnexcludeBankTransactions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks a bank transaction as excluded.
|
* Marks a bank transaction as excluded.
|
||||||
* @param {number} tenantId - The ID of the tenant.
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
@@ -56,4 +64,36 @@ export class ExcludeBankTransactionsApplication {
|
|||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude the given bank transactions in bulk.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {Array<number> | number} bankTransactionIds
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public excludeBankTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
bankTransactionIds: Array<number> | number
|
||||||
|
): Promise<void> {
|
||||||
|
return this.excludeBankTransactionsService.excludeBankTransactions(
|
||||||
|
tenantId,
|
||||||
|
bankTransactionIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude the given bank transactions in bulk.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {Array<number> | number} bankTransactionIds
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public unexcludeBankTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
bankTransactionIds: Array<number> | number
|
||||||
|
): Promise<void> {
|
||||||
|
return this.unexcludeBankTransactionsService.unexcludeBankTransactions(
|
||||||
|
tenantId,
|
||||||
|
bankTransactionIds
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import PromisePool from '@supercharge/promise-pool';
|
||||||
|
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
||||||
|
import { castArray } from 'lodash';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class UnexcludeBankTransactions {
|
||||||
|
@Inject()
|
||||||
|
private unexcludeBankTransaction: UnexcludeBankTransaction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unexclude bank transactions in bulk.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} bankTransactionIds
|
||||||
|
*/
|
||||||
|
public async unexcludeBankTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
bankTransactionIds: Array<number> | number
|
||||||
|
) {
|
||||||
|
const _bankTransactionIds = castArray(bankTransactionIds);
|
||||||
|
|
||||||
|
await PromisePool.withConcurrency(1)
|
||||||
|
.for(_bankTransactionIds)
|
||||||
|
.process((bankTransactionId: number) => {
|
||||||
|
return this.unexcludeBankTransaction.unexcludeBankTransaction(
|
||||||
|
tenantId,
|
||||||
|
bankTransactionId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ export class PlaidItemService {
|
|||||||
const { PlaidItem } = this.tenancy.models(tenantId);
|
const { PlaidItem } = this.tenancy.models(tenantId);
|
||||||
const { publicToken, institutionId } = itemDTO;
|
const { publicToken, institutionId } = itemDTO;
|
||||||
|
|
||||||
const plaidInstance = new PlaidClientWrapper();
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
|
|
||||||
// Exchange the public token for a private access token and store with the item.
|
// Exchange the public token for a private access token and store with the item.
|
||||||
const response = await plaidInstance.itemPublicTokenExchange({
|
const response = await plaidInstance.itemPublicTokenExchange({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class PlaidLinkTokenService {
|
|||||||
webhook: config.plaid.linkWebhook,
|
webhook: config.plaid.linkWebhook,
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
};
|
};
|
||||||
const plaidInstance = new PlaidClientWrapper();
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams);
|
const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams);
|
||||||
|
|
||||||
return createResponse.data;
|
return createResponse.data;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import * as R from 'ramda';
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import bluebird from 'bluebird';
|
import bluebird from 'bluebird';
|
||||||
import { entries, groupBy } from 'lodash';
|
import { entries, groupBy } from 'lodash';
|
||||||
|
import {
|
||||||
|
AccountBase as PlaidAccountBase,
|
||||||
|
Item as PlaidItem,
|
||||||
|
Institution as PlaidInstitution,
|
||||||
|
} from 'plaid';
|
||||||
import { CreateAccount } from '@/services/Accounts/CreateAccount';
|
import { CreateAccount } from '@/services/Accounts/CreateAccount';
|
||||||
import {
|
import {
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
@@ -53,6 +58,7 @@ export class PlaidSyncDb {
|
|||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const plaidAccount = await Account.query().findOne(
|
const plaidAccount = await Account.query().findOne(
|
||||||
'plaidAccountId',
|
'plaidAccountId',
|
||||||
createBankAccountDTO.plaidAccountId
|
createBankAccountDTO.plaidAccountId
|
||||||
@@ -77,13 +83,15 @@ export class PlaidSyncDb {
|
|||||||
*/
|
*/
|
||||||
public async syncBankAccounts(
|
public async syncBankAccounts(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
plaidAccounts: PlaidAccount[],
|
plaidAccounts: PlaidAccountBase[],
|
||||||
institution: any,
|
institution: PlaidInstitution,
|
||||||
|
item: PlaidItem,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const transformToPlaidAccounts =
|
const transformToPlaidAccounts = transformPlaidAccountToCreateAccount(
|
||||||
transformPlaidAccountToCreateAccount(institution);
|
item,
|
||||||
|
institution
|
||||||
|
);
|
||||||
const accountCreateDTOs = R.map(transformToPlaidAccounts)(plaidAccounts);
|
const accountCreateDTOs = R.map(transformToPlaidAccounts)(plaidAccounts);
|
||||||
|
|
||||||
await bluebird.map(
|
await bluebird.map(
|
||||||
@@ -148,7 +156,6 @@ export class PlaidSyncDb {
|
|||||||
*/
|
*/
|
||||||
public async syncAccountsTransactions(
|
public async syncAccountsTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
batchNo: string,
|
|
||||||
plaidAccountsTransactions: PlaidTransaction[],
|
plaidAccountsTransactions: PlaidTransaction[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -161,7 +168,6 @@ export class PlaidSyncDb {
|
|||||||
return this.syncAccountTranactions(
|
return this.syncAccountTranactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
plaidAccountId,
|
plaidAccountId,
|
||||||
batchNo,
|
|
||||||
plaidTransactions,
|
plaidTransactions,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class PlaidUpdateTransactions {
|
|||||||
await this.fetchTransactionUpdates(tenantId, plaidItemId);
|
await this.fetchTransactionUpdates(tenantId, plaidItemId);
|
||||||
|
|
||||||
const request = { access_token: accessToken };
|
const request = { access_token: accessToken };
|
||||||
const plaidInstance = new PlaidClientWrapper();
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
const {
|
const {
|
||||||
data: { accounts, item },
|
data: { accounts, item },
|
||||||
} = await plaidInstance.accountsGet(request);
|
} = await plaidInstance.accountsGet(request);
|
||||||
@@ -66,15 +66,19 @@ export class PlaidUpdateTransactions {
|
|||||||
country_codes: ['US', 'UK'],
|
country_codes: ['US', 'UK'],
|
||||||
});
|
});
|
||||||
// Sync bank accounts.
|
// Sync bank accounts.
|
||||||
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution, trx);
|
await this.plaidSync.syncBankAccounts(
|
||||||
|
tenantId,
|
||||||
|
accounts,
|
||||||
|
institution,
|
||||||
|
item,
|
||||||
|
trx
|
||||||
|
);
|
||||||
// Sync bank account transactions.
|
// Sync bank account transactions.
|
||||||
await this.plaidSync.syncAccountsTransactions(
|
await this.plaidSync.syncAccountsTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
added.concat(modified),
|
added.concat(modified),
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
// Sync removed transactions.
|
|
||||||
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
|
|
||||||
// Sync transactions cursor.
|
// Sync transactions cursor.
|
||||||
await this.plaidSync.syncTransactionsCursor(
|
await this.plaidSync.syncTransactionsCursor(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -143,7 +147,7 @@ export class PlaidUpdateTransactions {
|
|||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
count: batchSize,
|
count: batchSize,
|
||||||
};
|
};
|
||||||
const plaidInstance = new PlaidClientWrapper();
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
const response = await plaidInstance.transactionsSync(request);
|
const response = await plaidInstance.transactionsSync(request);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
// Add this page of results
|
// Add this page of results
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
import {
|
||||||
|
Item as PlaidItem,
|
||||||
|
Institution as PlaidInstitution,
|
||||||
|
AccountBase as PlaidAccount,
|
||||||
|
} from 'plaid';
|
||||||
import {
|
import {
|
||||||
CreateUncategorizedTransactionDTO,
|
CreateUncategorizedTransactionDTO,
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
PlaidAccount,
|
|
||||||
PlaidTransaction,
|
PlaidTransaction,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the Plaid account to create cashflow account DTO.
|
* Transformes the Plaid account to create cashflow account DTO.
|
||||||
* @param {PlaidAccount} plaidAccount
|
* @param {PlaidItem} item -
|
||||||
|
* @param {PlaidInstitution} institution -
|
||||||
|
* @param {PlaidAccount} plaidAccount -
|
||||||
* @returns {IAccountCreateDTO}
|
* @returns {IAccountCreateDTO}
|
||||||
*/
|
*/
|
||||||
export const transformPlaidAccountToCreateAccount = R.curry(
|
export const transformPlaidAccountToCreateAccount = R.curry(
|
||||||
(institution: any, plaidAccount: PlaidAccount): IAccountCreateDTO => {
|
(
|
||||||
|
item: PlaidItem,
|
||||||
|
institution: PlaidInstitution,
|
||||||
|
plaidAccount: PlaidAccount
|
||||||
|
): IAccountCreateDTO => {
|
||||||
return {
|
return {
|
||||||
name: `${institution.name} - ${plaidAccount.name}`,
|
name: `${institution.name} - ${plaidAccount.name}`,
|
||||||
code: '',
|
code: '',
|
||||||
@@ -20,9 +30,10 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
currencyCode: plaidAccount.balances.iso_currency_code,
|
currencyCode: plaidAccount.balances.iso_currency_code,
|
||||||
accountType: 'cash',
|
accountType: 'cash',
|
||||||
active: true,
|
active: true,
|
||||||
plaidAccountId: plaidAccount.account_id,
|
|
||||||
bankBalance: plaidAccount.balances.current,
|
bankBalance: plaidAccount.balances.current,
|
||||||
accountMask: plaidAccount.mask,
|
accountMask: plaidAccount.mask,
|
||||||
|
plaidAccountId: plaidAccount.account_id,
|
||||||
|
plaidItemId: item.item_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -37,7 +48,6 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||||
(
|
(
|
||||||
cashflowAccountId: number,
|
cashflowAccountId: number,
|
||||||
creditAccountId: number,
|
|
||||||
plaidTranasction: PlaidTransaction
|
plaidTranasction: PlaidTransaction
|
||||||
): CreateUncategorizedTransactionDTO => {
|
): CreateUncategorizedTransactionDTO => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { Knex } from 'knex';
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import UnitOfWork from '../UnitOfWork';
|
import UnitOfWork from '../UnitOfWork';
|
||||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
CreateUncategorizedTransactionDTO,
|
||||||
|
IUncategorizedTransactionCreatedEventPayload,
|
||||||
|
IUncategorizedTransactionCreatingEventPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CreateUncategorizedTransaction {
|
export class CreateUncategorizedTransaction {
|
||||||
@@ -12,6 +18,9 @@ export class CreateUncategorizedTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an uncategorized cashflow transaction.
|
* Creates an uncategorized cashflow transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -19,7 +28,7 @@ export class CreateUncategorizedTransaction {
|
|||||||
*/
|
*/
|
||||||
public create(
|
public create(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
createDTO: CreateUncategorizedTransactionDTO,
|
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
@@ -27,12 +36,30 @@ export class CreateUncategorizedTransaction {
|
|||||||
return this.uow.withTransaction(
|
return this.uow.withTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
async (trx: Knex.Transaction) => {
|
async (trx: Knex.Transaction) => {
|
||||||
const transaction = await UncategorizedCashflowTransaction.query(
|
await this.eventPublisher.emitAsync(
|
||||||
trx
|
events.cashflow.onTransactionUncategorizedCreating,
|
||||||
).insertAndFetch({
|
{
|
||||||
...createDTO,
|
tenantId,
|
||||||
});
|
createUncategorizedTransactionDTO,
|
||||||
return transaction;
|
trx,
|
||||||
|
} as IUncategorizedTransactionCreatingEventPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
const uncategorizedTransaction =
|
||||||
|
await UncategorizedCashflowTransaction.query(trx).insertAndFetch({
|
||||||
|
...createUncategorizedTransactionDTO,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionUncategorizedCreated,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
createUncategorizedTransactionDTO,
|
||||||
|
trx,
|
||||||
|
} as IUncategorizedTransactionCreatedEventPayload
|
||||||
|
);
|
||||||
|
return uncategorizedTransaction;
|
||||||
},
|
},
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export class DecrementUncategorizedTransactionOnCategorize {
|
|||||||
events.cashflow.onTransactionUncategorized,
|
events.cashflow.onTransactionUncategorized,
|
||||||
this.incrementUnCategorizedTransactionsOnUncategorized.bind(this)
|
this.incrementUnCategorizedTransactionsOnUncategorized.bind(this)
|
||||||
);
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.cashflow.onTransactionUncategorizedCreated,
|
||||||
|
this.incrementUncategoirzedTransactionsOnCreated.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,4 +57,22 @@ export class DecrementUncategorizedTransactionOnCategorize {
|
|||||||
.findById(uncategorizedTransaction.accountId)
|
.findById(uncategorizedTransaction.accountId)
|
||||||
.increment('uncategorizedTransactions', 1);
|
.increment('uncategorizedTransactions', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments uncategorized transactions count once creating a new transaction.
|
||||||
|
* @param {ICommandCashflowCreatedPayload} payload -
|
||||||
|
*/
|
||||||
|
public async incrementUncategoirzedTransactionsOnCreated({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
trx,
|
||||||
|
}: any) {
|
||||||
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
if (!uncategorizedTransaction.accountId) return;
|
||||||
|
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(uncategorizedTransaction.accountId)
|
||||||
|
.increment('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new customer.
|
* Creates a new customer.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {ICustomerNewDTO} customerDTO
|
* @param {ICustomerNewDTO} customerDTO
|
||||||
* @param {ISystemUser} authorizedUser
|
* @param {ISystemUser} authorizedUser
|
||||||
* @returns {Promise<ICustomer>}
|
* @returns {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
|
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
|
||||||
@@ -56,9 +56,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits details of the given customer.
|
* Edits details of the given customer.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} customerId
|
* @param {number} customerId
|
||||||
* @param {ICustomerEditDTO} customerDTO
|
* @param {ICustomerEditDTO} customerDTO
|
||||||
* @return {Promise<ICustomer>}
|
* @return {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public editCustomer = (
|
public editCustomer = (
|
||||||
@@ -75,9 +75,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the given customer and associated transactions.
|
* Deletes the given customer and associated transactions.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} customerId
|
* @param {number} customerId
|
||||||
* @param {ISystemUser} authorizedUser
|
* @param {ISystemUser} authorizedUser
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public deleteCustomer = (
|
public deleteCustomer = (
|
||||||
@@ -94,9 +94,9 @@ export class CustomersApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the opening balance of the given customer.
|
* Changes the opening balance of the given customer.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} customerId
|
* @param {number} customerId
|
||||||
* @param {Date|string} openingBalanceEditDTO
|
* @param {Date|string} openingBalanceEditDTO
|
||||||
* @returns {Promise<ICustomer>}
|
* @returns {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public editOpeningBalance = (
|
public editOpeningBalance = (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { IFeatureAllItem, ISystemUser } from '@/interfaces';
|
import { IFeatureAllItem, ISystemUser } from '@/interfaces';
|
||||||
import { FeaturesManager } from '@/services/Features/FeaturesManager';
|
import { FeaturesManager } from '@/services/Features/FeaturesManager';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
import config from '@/config';
|
||||||
|
|
||||||
interface IRoleAbility {
|
interface IRoleAbility {
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -11,15 +12,16 @@ interface IRoleAbility {
|
|||||||
interface IDashboardBootMeta {
|
interface IDashboardBootMeta {
|
||||||
abilities: IRoleAbility[];
|
abilities: IRoleAbility[];
|
||||||
features: IFeatureAllItem[];
|
features: IFeatureAllItem[];
|
||||||
|
isBigcapitalCloud: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DashboardService {
|
export default class DashboardService {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
featuresManager: FeaturesManager;
|
private featuresManager: FeaturesManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve dashboard meta.
|
* Retrieve dashboard meta.
|
||||||
@@ -39,6 +41,7 @@ export default class DashboardService {
|
|||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
features,
|
features,
|
||||||
|
isBigcapitalCloud: config.hostedOnBigcapitalCloud
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
51
packages/server/src/services/Loops/LoopsEventsSubscriber.ts
Normal file
51
packages/server/src/services/Loops/LoopsEventsSubscriber.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import config from '@/config';
|
||||||
|
import { IAuthSignUpVerifiedEventPayload } from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
|
||||||
|
export class LoopsEventsSubscriber {
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.auth.signUpConfirmed,
|
||||||
|
this.triggerEventOnSignupVerified.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once the user verified sends the event to the Loops.
|
||||||
|
* @param {IAuthSignUpVerifiedEventPayload} param0
|
||||||
|
*/
|
||||||
|
public async triggerEventOnSignupVerified({
|
||||||
|
email,
|
||||||
|
userId,
|
||||||
|
}: IAuthSignUpVerifiedEventPayload) {
|
||||||
|
// Can't continue since the Loops the api key is not configured.
|
||||||
|
if (!config.loops.apiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = await SystemUser.query().findById(userId);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://app.loops.so/api/v1/events/send',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.loops.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
userId,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
eventName: 'USER_VERIFIED',
|
||||||
|
eventProperties: {},
|
||||||
|
mailingLists: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await axios(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { omit, sumBy } from 'lodash';
|
|||||||
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
|
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
|
||||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||||
import { formatDateFields } from '@/utils';
|
import { formatDateFields } from '@/utils';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandBillPaymentDTOTransformer {
|
export class CommandBillPaymentDTOTransformer {
|
||||||
@@ -23,11 +24,14 @@ export class CommandBillPaymentDTOTransformer {
|
|||||||
vendor: IVendor,
|
vendor: IVendor,
|
||||||
oldBillPayment?: IBillPayment
|
oldBillPayment?: IBillPayment
|
||||||
): Promise<IBillPayment> {
|
): Promise<IBillPayment> {
|
||||||
|
const amount =
|
||||||
|
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
|
||||||
|
|
||||||
const initialDTO = {
|
const initialDTO = {
|
||||||
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
|
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
|
||||||
'paymentDate',
|
'paymentDate',
|
||||||
]),
|
]),
|
||||||
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
|
amount,
|
||||||
currencyCode: vendor.currencyCode,
|
currencyCode: vendor.currencyCode,
|
||||||
exchangeRate: billPaymentDTO.exchangeRate || 1,
|
exchangeRate: billPaymentDTO.exchangeRate || 1,
|
||||||
entries: billPaymentDTO.entries,
|
entries: billPaymentDTO.entries,
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export class PaymentReceiveDTOTransformer {
|
|||||||
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
|
||||||
oldPaymentReceive?: IPaymentReceive
|
oldPaymentReceive?: IPaymentReceive
|
||||||
): Promise<IPaymentReceive> {
|
): Promise<IPaymentReceive> {
|
||||||
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
const amount =
|
||||||
|
paymentReceiveDTO.amount ??
|
||||||
|
sumBy(paymentReceiveDTO.entries, 'paymentAmount');
|
||||||
|
|
||||||
// Retreive the next invoice number.
|
// Retreive the next invoice number.
|
||||||
const autoNextNumber =
|
const autoNextNumber =
|
||||||
@@ -54,7 +56,7 @@ export class PaymentReceiveDTOTransformer {
|
|||||||
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
|
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
|
||||||
'paymentDate',
|
'paymentDate',
|
||||||
]),
|
]),
|
||||||
amount: paymentAmount,
|
amount,
|
||||||
currencyCode: customer.currencyCode,
|
currencyCode: customer.currencyCode,
|
||||||
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
|
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
|
||||||
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetSubscriptionsTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale invoice object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [
|
||||||
|
'canceledAtFormatted',
|
||||||
|
'endsAtFormatted',
|
||||||
|
'trialStartsAtFormatted',
|
||||||
|
'trialEndsAtFormatted',
|
||||||
|
'statusFormatted',
|
||||||
|
'planName',
|
||||||
|
'planSlug',
|
||||||
|
'planPrice',
|
||||||
|
'planPriceCurrency',
|
||||||
|
'planPriceFormatted',
|
||||||
|
'planPeriod',
|
||||||
|
'lemonUrls',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['id', 'plan'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the canceled at formatted.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public canceledAtFormatted = (subscription) => {
|
||||||
|
return subscription.canceledAt
|
||||||
|
? this.formatDate(subscription.canceledAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the ends at date formatted.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public endsAtFormatted = (subscription) => {
|
||||||
|
return subscription.cancelsAt
|
||||||
|
? this.formatDate(subscription.endsAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial starts at formatted date.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public trialStartsAtFormatted = (subscription) => {
|
||||||
|
return subscription.trialStartsAt
|
||||||
|
? this.formatDate(subscription.trialStartsAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial ends at formatted date.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public trialEndsAtFormatted = (subscription) => {
|
||||||
|
return subscription.trialEndsAt
|
||||||
|
? this.formatDate(subscription.trialEndsAt)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Lemon subscription metadata.
|
||||||
|
* @param subscription
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public lemonSubscription = (subscription) => {
|
||||||
|
return (
|
||||||
|
this.options.lemonSubscriptions[subscription.lemonSubscriptionId] || null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted subscription status.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public statusFormatted = (subscription) => {
|
||||||
|
const pairs = {
|
||||||
|
canceled: 'Canceled',
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive',
|
||||||
|
expired: 'Expired',
|
||||||
|
on_trial: 'On Trial',
|
||||||
|
};
|
||||||
|
return pairs[subscription.status] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan name.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planName(subscription) {
|
||||||
|
return subscription.plan?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan slug.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planSlug(subscription) {
|
||||||
|
return subscription.plan?.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan price.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public planPrice(subscription) {
|
||||||
|
return subscription.plan?.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan price currency.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planPriceCurrency(subscription) {
|
||||||
|
return subscription.plan?.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan formatted price.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planPriceFormatted(subscription) {
|
||||||
|
return this.formatMoney(subscription.plan?.price, {
|
||||||
|
currencyCode: subscription.plan?.currency,
|
||||||
|
precision: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription plan period.
|
||||||
|
* @param subscription
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public planPeriod(subscription) {
|
||||||
|
return subscription?.plan?.period;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the subscription Lemon Urls.
|
||||||
|
* @param subscription
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public lemonUrls = (subscription) => {
|
||||||
|
const lemonSusbcription = this.lemonSubscription(subscription);
|
||||||
|
return lemonSusbcription?.data?.attributes?.urls;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { cancelSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
import { configureLemonSqueezy } from './utils';
|
||||||
|
import { PlanSubscription } from '@/system/models';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { ERRORS, IOrganizationSubscriptionCanceled } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class LemonCancelSubscription {
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the subscription of the given tenant.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} subscriptionId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async cancelSubscription(tenantId: number) {
|
||||||
|
configureLemonSqueezy();
|
||||||
|
|
||||||
|
const subscription = await PlanSubscription.query().findOne({
|
||||||
|
tenantId,
|
||||||
|
slug: 'main',
|
||||||
|
});
|
||||||
|
if (!subscription) {
|
||||||
|
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
|
||||||
|
}
|
||||||
|
const lemonSusbcriptionId = subscription.lemonSubscriptionId;
|
||||||
|
const subscriptionId = subscription.id;
|
||||||
|
const cancelledSub = await cancelSubscription(lemonSusbcriptionId);
|
||||||
|
|
||||||
|
if (cancelledSub.error) {
|
||||||
|
throw new Error(cancelledSub.error.message);
|
||||||
|
}
|
||||||
|
await PlanSubscription.query().findById(subscriptionId).patch({
|
||||||
|
canceledAt: new Date(),
|
||||||
|
});
|
||||||
|
// Triggers `onSubscriptionCanceled` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.subscription.onSubscriptionCanceled,
|
||||||
|
{ tenantId, subscriptionId } as IOrganizationSubscriptionCanceled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { updateSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { PlanSubscription } from '@/system/models';
|
||||||
|
import { configureLemonSqueezy } from './utils';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { IOrganizationSubscriptionChanged } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class LemonChangeSubscriptionPlan {
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the given organization subscription plan.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} newVariantId - New variant id.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async changeSubscriptionPlan(tenantId: number, newVariantId: number) {
|
||||||
|
configureLemonSqueezy();
|
||||||
|
|
||||||
|
const subscription = await PlanSubscription.query().findOne({
|
||||||
|
tenantId,
|
||||||
|
slug: 'main',
|
||||||
|
});
|
||||||
|
const lemonSubscriptionId = subscription.lemonSubscriptionId;
|
||||||
|
|
||||||
|
// Send request to Lemon Squeezy to change the subscription.
|
||||||
|
const updatedSub = await updateSubscription(lemonSubscriptionId, {
|
||||||
|
variantId: newVariantId,
|
||||||
|
});
|
||||||
|
if (updatedSub.error) {
|
||||||
|
throw new ServiceError('SOMETHING_WENT_WRONG');
|
||||||
|
}
|
||||||
|
// Triggers `onSubscriptionPlanChanged` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.subscription.onSubscriptionPlanChanged,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
lemonSubscriptionId,
|
||||||
|
newVariantId,
|
||||||
|
} as IOrganizationSubscriptionChanged
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { configureLemonSqueezy } from './utils';
|
||||||
|
import { PlanSubscription } from '@/system/models';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { ERRORS, IOrganizationSubscriptionResumed } from './types';
|
||||||
|
import { updateSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class LemonResumeSubscription {
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumes the main subscription of the given tenant.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async resumeSubscription(tenantId: number) {
|
||||||
|
configureLemonSqueezy();
|
||||||
|
|
||||||
|
const subscription = await PlanSubscription.query().findOne({
|
||||||
|
tenantId,
|
||||||
|
slug: 'main',
|
||||||
|
});
|
||||||
|
if (!subscription) {
|
||||||
|
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
|
||||||
|
}
|
||||||
|
const subscriptionId = subscription.id;
|
||||||
|
const lemonSubscriptionId = subscription.lemonSubscriptionId;
|
||||||
|
const returnedSub = await updateSubscription(lemonSubscriptionId, {
|
||||||
|
cancelled: false,
|
||||||
|
});
|
||||||
|
if (returnedSub.error) {
|
||||||
|
throw new ServiceError('');
|
||||||
|
}
|
||||||
|
// Update the subscription of the organization.
|
||||||
|
await PlanSubscription.query().findById(subscriptionId).patch({
|
||||||
|
canceledAt: null,
|
||||||
|
});
|
||||||
|
// Triggers `onSubscriptionCanceled` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.subscription.onSubscriptionResumed,
|
||||||
|
{ tenantId, subscriptionId } as IOrganizationSubscriptionResumed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +9,6 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
import { Plan } from '@/system/models';
|
import { Plan } from '@/system/models';
|
||||||
import { Subscription } from './Subscription';
|
import { Subscription } from './Subscription';
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class LemonSqueezyWebhooks {
|
export class LemonSqueezyWebhooks {
|
||||||
@@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks {
|
|||||||
private subscriptionService: Subscription;
|
private subscriptionService: Subscription;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle the LemonSqueezy webhooks.
|
* Handles the Lemon Squeezy webhooks.
|
||||||
* @param {string} rawBody
|
* @param {string} rawBody
|
||||||
* @param {string} signature
|
* @param {string} signature
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks {
|
|||||||
const variantId = attributes.variant_id as string;
|
const variantId = attributes.variant_id as string;
|
||||||
|
|
||||||
// We assume that the Plan table is up to date.
|
// We assume that the Plan table is up to date.
|
||||||
const plan = await Plan.query().findOne('slug', 'early-adaptor');
|
const plan = await Plan.query().findOne('lemonVariantId', variantId);
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
throw new Error(`Plan with variantId ${variantId} not found.`);
|
throw new Error(`Plan with variantId ${variantId} not found.`);
|
||||||
@@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks {
|
|||||||
// Update the subscription in the database.
|
// Update the subscription in the database.
|
||||||
const priceId = attributes.first_subscription_item.price_id;
|
const priceId = attributes.first_subscription_item.price_id;
|
||||||
|
|
||||||
// Get the price data from Lemon Squeezy.
|
|
||||||
const priceData = await getPrice(priceId);
|
|
||||||
|
|
||||||
if (priceData.error) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to get the price data for the subscription ${eventBody.data.id}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const isUsageBased =
|
|
||||||
attributes.first_subscription_item.is_usage_based;
|
|
||||||
const price = isUsageBased
|
|
||||||
? priceData.data?.data.attributes.unit_price_decimal
|
|
||||||
: priceData.data?.data.attributes.unit_price;
|
|
||||||
|
|
||||||
// Create a new subscription of the tenant.
|
// Create a new subscription of the tenant.
|
||||||
if (webhookEvent === 'subscription_created') {
|
if (webhookEvent === 'subscription_created') {
|
||||||
await this.subscriptionService.newSubscribtion(
|
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
|
||||||
tenantId,
|
|
||||||
'early-adaptor'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (webhookEvent.startsWith('order_')) {
|
} else if (webhookEvent.startsWith('order_')) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { LemonCancelSubscription } from './LemonCancelSubscription';
|
||||||
|
import { LemonChangeSubscriptionPlan } from './LemonChangeSubscriptionPlan';
|
||||||
|
import { LemonResumeSubscription } from './LemonResumeSubscription';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SubscriptionApplication {
|
||||||
|
@Inject()
|
||||||
|
private cancelSubscriptionService: LemonCancelSubscription;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private resumeSubscriptionService: LemonResumeSubscription;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private changeSubscriptionPlanService: LemonChangeSubscriptionPlan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the subscription of the given tenant.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public cancelSubscription(tenantId: number, id: string) {
|
||||||
|
return this.cancelSubscriptionService.cancelSubscription(tenantId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumes the subscription of the given tenant.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public resumeSubscription(tenantId: number) {
|
||||||
|
return this.resumeSubscriptionService.resumeSubscription(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the given organization subscription plan.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} newVariantId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public changeSubscriptionPlan(tenantId: number, newVariantId: number) {
|
||||||
|
return this.changeSubscriptionPlanService.changeSubscriptionPlan(
|
||||||
|
tenantId,
|
||||||
|
newVariantId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,50 @@
|
|||||||
import { Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { getSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { PlanSubscription } from '@/system/models';
|
import { PlanSubscription } from '@/system/models';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { GetSubscriptionsTransformer } from './GetSubscriptionsTransformer';
|
||||||
|
import { configureLemonSqueezy } from './utils';
|
||||||
|
import { fromPairs } from 'lodash';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all subscription of the given tenant.
|
* Retrieve all subscription of the given tenant.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
*/
|
*/
|
||||||
public async getSubscriptions(tenantId: number) {
|
public async getSubscriptions(tenantId: number) {
|
||||||
const subscriptions = await PlanSubscription.query().where(
|
configureLemonSqueezy();
|
||||||
'tenant_id',
|
|
||||||
tenantId
|
const subscriptions = await PlanSubscription.query()
|
||||||
|
.where('tenant_id', tenantId)
|
||||||
|
.withGraphFetched('plan');
|
||||||
|
|
||||||
|
const lemonSubscriptionsResult = await PromisePool.withConcurrency(1)
|
||||||
|
.for(subscriptions)
|
||||||
|
.process(async (subscription, index, pool) => {
|
||||||
|
if (subscription.lemonSubscriptionId) {
|
||||||
|
const res = await getSubscription(subscription.lemonSubscriptionId);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return [subscription.lemonSubscriptionId, res.data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lemonSubscriptions = fromPairs(
|
||||||
|
lemonSubscriptionsResult?.results.filter((result) => !!result[1])
|
||||||
|
);
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
subscriptions,
|
||||||
|
new GetSubscriptionsTransformer(),
|
||||||
|
{
|
||||||
|
lemonSubscriptions,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return subscriptions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/server/src/services/Subscription/types.ts
Normal file
20
packages/server/src/services/Subscription/types.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export const ERRORS = {
|
||||||
|
SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT:
|
||||||
|
'SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IOrganizationSubscriptionChanged {
|
||||||
|
tenantId: number;
|
||||||
|
lemonSubscriptionId: string;
|
||||||
|
newVariantId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationSubscriptionCanceled {
|
||||||
|
tenantId: number;
|
||||||
|
subscriptionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrganizationSubscriptionResumed {
|
||||||
|
tenantId: number;
|
||||||
|
subscriptionId: number;
|
||||||
|
}
|
||||||
@@ -77,7 +77,12 @@ export default class HasTenancyService {
|
|||||||
const knex = this.knex(tenantId);
|
const knex = this.knex(tenantId);
|
||||||
const i18n = this.i18n(tenantId);
|
const i18n = this.i18n(tenantId);
|
||||||
|
|
||||||
return tenantRepositoriesLoader(knex, cache, i18n);
|
const repositories = tenantRepositoriesLoader(knex, cache, i18n);
|
||||||
|
|
||||||
|
Object.values(repositories).forEach((repository) => {
|
||||||
|
repository.setTenantId(tenantId);
|
||||||
|
});
|
||||||
|
return repositories;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ export default {
|
|||||||
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization subscription.
|
||||||
|
*/
|
||||||
|
subscription: {
|
||||||
|
onSubscriptionCanceled: 'onSubscriptionCanceled',
|
||||||
|
onSubscriptionResumed: 'onSubscriptionResumed',
|
||||||
|
onSubscriptionPlanChanged: 'onSubscriptionPlanChanged',
|
||||||
|
onSubscribed: 'onOrganizationSubscribed',
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenants managment service.
|
* Tenants managment service.
|
||||||
*/
|
*/
|
||||||
@@ -399,6 +409,9 @@ export default {
|
|||||||
onTransactionCategorizing: 'onTransactionCategorizing',
|
onTransactionCategorizing: 'onTransactionCategorizing',
|
||||||
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
||||||
|
|
||||||
|
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
|
||||||
|
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
|
||||||
|
|
||||||
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
||||||
onTransactionUncategorized: 'onTransactionUncategorized',
|
onTransactionUncategorized: 'onTransactionUncategorized',
|
||||||
|
|
||||||
@@ -648,6 +661,11 @@ export default {
|
|||||||
onUnexcluded: 'onBankTransactionUnexcluded',
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bankAccount: {
|
||||||
|
onDisconnecting: 'onBankAccountDisconnecting',
|
||||||
|
onDisconnected: 'onBankAccountDisconnected',
|
||||||
|
},
|
||||||
|
|
||||||
// Import files.
|
// Import files.
|
||||||
import: {
|
import: {
|
||||||
onImportCommitted: 'onImportFileCommitted',
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plans', (table) => {
|
||||||
|
table.string('lemon_variant_id').nullable().index();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (knex) => {
|
||||||
|
return knex.schema.table('subscription_plans', (table) => {
|
||||||
|
table.dropColumn('lemon_variant_id');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex('subscription_plans').insert([
|
||||||
|
// Capital Basic
|
||||||
|
{
|
||||||
|
name: 'Capital Basic (Monthly)',
|
||||||
|
slug: 'capital-basic-monthly',
|
||||||
|
price: 10,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446152',
|
||||||
|
// lemon_variant_id: '450016',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Basic (Annually)',
|
||||||
|
slug: 'capital-basic-annually',
|
||||||
|
price: 90,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446153',
|
||||||
|
// lemon_variant_id: '450018',
|
||||||
|
},
|
||||||
|
|
||||||
|
// # Capital Essential
|
||||||
|
{
|
||||||
|
name: 'Capital Essential (Monthly)',
|
||||||
|
slug: 'capital-essential-monthly',
|
||||||
|
price: 20,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446155',
|
||||||
|
// lemon_variant_id: '450028',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Essential (Annually)',
|
||||||
|
slug: 'capital-essential-annually',
|
||||||
|
price: 180,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446156',
|
||||||
|
// lemon_variant_id: '450029',
|
||||||
|
},
|
||||||
|
|
||||||
|
// # Capital Plus
|
||||||
|
{
|
||||||
|
name: 'Capital Plus (Monthly)',
|
||||||
|
slug: 'capital-plus-monthly',
|
||||||
|
price: 25,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446165',
|
||||||
|
// lemon_variant_id: '450031',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Plus (Annually)',
|
||||||
|
slug: 'capital-plus-annually',
|
||||||
|
price: 228,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446164',
|
||||||
|
// lemon_variant_id: '450032',
|
||||||
|
},
|
||||||
|
|
||||||
|
// # Capital Big
|
||||||
|
{
|
||||||
|
name: 'Capital Big (Monthly)',
|
||||||
|
slug: 'capital-big-monthly',
|
||||||
|
price: 40,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
lemon_variant_id: '446167',
|
||||||
|
// lemon_variant_id: '450024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Big (Annually)',
|
||||||
|
slug: 'capital-big-annually',
|
||||||
|
price: 360,
|
||||||
|
active: true,
|
||||||
|
invoice_period: 1,
|
||||||
|
invoice_interval: 'year',
|
||||||
|
lemon_variant_id: '446168',
|
||||||
|
// lemon_variant_id: '450025',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plan_subscriptions', (table) => {
|
||||||
|
table.string('lemon_subscription_id').nullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plan_subscriptions', (table) => {
|
||||||
|
table.dropColumn('lemon_subscription_id');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plan_subscriptions', (table) => {
|
||||||
|
table.dateTime('trial_ends_at').nullable();
|
||||||
|
table.dropColumn('cancels_at');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('subscription_plan_subscriptions', (table) => {
|
||||||
|
table.dropColumn('trial_ends_at').nullable();
|
||||||
|
table.dateTime('cancels_at').nullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -4,6 +4,15 @@ import moment from 'moment';
|
|||||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||||
|
|
||||||
export default class PlanSubscription extends mixin(SystemModel) {
|
export default class PlanSubscription extends mixin(SystemModel) {
|
||||||
|
public lemonSubscriptionId: number;
|
||||||
|
|
||||||
|
public endsAt: Date;
|
||||||
|
public startsAt: Date;
|
||||||
|
|
||||||
|
public canceledAt: Date;
|
||||||
|
|
||||||
|
public trialEndsAt: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +31,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
* Defined virtual attributes.
|
* Defined virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['active', 'inactive', 'ended', 'onTrial'];
|
return ['active', 'inactive', 'ended', 'canceled', 'onTrial', 'status'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +47,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
builder.where('trial_ends_at', '>', now);
|
builder.where('trial_ends_at', '>', now);
|
||||||
},
|
},
|
||||||
|
|
||||||
inactiveSubscriptions() {
|
inactiveSubscriptions(builder) {
|
||||||
builder.modify('endedTrial');
|
builder.modify('endedTrial');
|
||||||
builder.modify('endedPeriod');
|
builder.modify('endedPeriod');
|
||||||
},
|
},
|
||||||
@@ -98,35 +107,65 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription is active.
|
* Check if the subscription is active.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
active() {
|
public active() {
|
||||||
return !this.ended() || this.onTrial();
|
return this.onTrial() || !this.ended();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription is inactive.
|
* Check if the subscription is inactive.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
inactive() {
|
public inactive() {
|
||||||
return !this.active();
|
return !this.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription period has ended.
|
* Check if paid subscription period has ended.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
ended() {
|
public ended() {
|
||||||
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the paid subscription has started.
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
public started() {
|
||||||
|
return this.startsAt ? moment().isAfter(this.startsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if subscription is currently on trial.
|
* Check if subscription is currently on trial.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
onTrial() {
|
public onTrial() {
|
||||||
return this.trailEndsAt ? moment().isAfter(this.trailEndsAt) : false;
|
return this.trialEndsAt ? moment().isBefore(this.trialEndsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the subscription is canceled.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public canceled() {
|
||||||
|
return !!this.canceledAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription status.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public status() {
|
||||||
|
return this.canceled()
|
||||||
|
? 'canceled'
|
||||||
|
: this.onTrial()
|
||||||
|
? 'on_trial'
|
||||||
|
: this.active()
|
||||||
|
? 'active'
|
||||||
|
: 'inactive';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,7 +180,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
const period = new SubscriptionPeriod(
|
const period = new SubscriptionPeriod(
|
||||||
invoiceInterval,
|
invoiceInterval,
|
||||||
invoicePeriod,
|
invoicePeriod,
|
||||||
start,
|
start
|
||||||
);
|
);
|
||||||
|
|
||||||
const startsAt = period.getStartDate();
|
const startsAt = period.getStartDate();
|
||||||
@@ -157,7 +196,7 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
renew(invoiceInterval, invoicePeriod) {
|
renew(invoiceInterval, invoicePeriod) {
|
||||||
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
||||||
invoiceInterval,
|
invoiceInterval,
|
||||||
invoicePeriod,
|
invoicePeriod
|
||||||
);
|
);
|
||||||
return this.$query().update({ startsAt, endsAt });
|
return this.$query().update({ startsAt, endsAt });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
import { Position, Toaster, Intent } from '@blueprintjs/core';
|
import { Position, Toaster, Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
export const AppToaster = Toaster.create({
|
export const AppToaster = Toaster.create({
|
||||||
position: Position.RIGHT_BOTTOM,
|
position: Position.TOP,
|
||||||
intent: Intent.WARNING,
|
intent: Intent.WARNING,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useAuthActions } from '@/hooks/state';
|
|||||||
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import { useAuthenticatedAccount } from '@/hooks/query';
|
import { useAuthenticatedAccount, useDashboardMeta } from '@/hooks/query';
|
||||||
import { firstLettersArgs, compose } from '@/utils';
|
import { firstLettersArgs, compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +31,9 @@ function DashboardTopbarUser({
|
|||||||
// Retrieve authenticated user information.
|
// Retrieve authenticated user information.
|
||||||
const { data: user } = useAuthenticatedAccount();
|
const { data: user } = useAuthenticatedAccount();
|
||||||
|
|
||||||
|
const { data: dashboardMeta } = useDashboardMeta({
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
const onClickLogout = () => {
|
const onClickLogout = () => {
|
||||||
setLogout();
|
setLogout();
|
||||||
};
|
};
|
||||||
@@ -58,6 +61,12 @@ function DashboardTopbarUser({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
|
{dashboardMeta.is_bigcapital_cloud && (
|
||||||
|
<MenuItem
|
||||||
|
text={'Billing'}
|
||||||
|
onClick={() => history.push('/billing')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
text={<T id={'keyboard_shortcuts'} />}
|
text={<T id={'keyboard_shortcuts'} />}
|
||||||
onClick={onKeyboardShortcut}
|
onClick={onKeyboardShortcut}
|
||||||
@@ -79,6 +88,4 @@ function DashboardTopbarUser({
|
|||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default compose(
|
export default compose(withDialogActions)(DashboardTopbarUser);
|
||||||
withDialogActions,
|
|
||||||
)(DashboardTopbarUser);
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCre
|
|||||||
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
|
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
|
||||||
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
||||||
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
||||||
|
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
|
||||||
|
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ export default function DrawersContainer() {
|
|||||||
/>
|
/>
|
||||||
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
||||||
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
||||||
|
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,10 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2F343C;
|
color: #2F343C;
|
||||||
|
|
||||||
@@ -47,13 +48,31 @@
|
|||||||
}
|
}
|
||||||
.price {
|
.price {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #404854;
|
color: #252A31;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricePer{
|
.pricePer{
|
||||||
color: #738091;
|
color: #738091;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureItem{
|
||||||
|
flex: 1;
|
||||||
|
color: #1C2127;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featurePopover :global .bp4-popover-content{
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.featurePopoverContent{
|
||||||
|
font-size: 12px
|
||||||
|
}
|
||||||
|
.featurePopoverLabel {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Button, ButtonProps, Intent } from '@blueprintjs/core';
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
Intent,
|
||||||
|
Position,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { Box, Group, Stack } from '../Layout';
|
import { Box, Group, Stack } from '../Layout';
|
||||||
import styles from './PricingPlan.module.scss';
|
import styles from './PricingPlan.module.scss';
|
||||||
@@ -64,7 +71,7 @@ export interface PricingPriceProps {
|
|||||||
*/
|
*/
|
||||||
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={6} className={styles.priceRoot}>
|
<Stack spacing={4} className={styles.priceRoot}>
|
||||||
<h4 className={styles.price}>{price}</h4>
|
<h4 className={styles.price}>{price}</h4>
|
||||||
<span className={styles.pricePer}>{subPrice}</span>
|
<span className={styles.pricePer}>{subPrice}</span>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -101,7 +108,7 @@ export interface PricingFeaturesProps {
|
|||||||
*/
|
*/
|
||||||
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={10} className={styles.features}>
|
<Stack spacing={14} className={styles.features}>
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -109,15 +116,41 @@ PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
|||||||
|
|
||||||
export interface PricingFeatureLineProps {
|
export interface PricingFeatureLineProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
hintContent?: string;
|
||||||
|
hintLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a single feature line within a list of features.
|
* Displays a single feature line within a list of features.
|
||||||
* @param children - The content of the feature line.
|
* @param children - The content of the feature line.
|
||||||
*/
|
*/
|
||||||
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
|
PricingPlan.FeatureLine = ({
|
||||||
return (
|
children,
|
||||||
<Group noWrap spacing={12}>
|
hintContent,
|
||||||
|
hintLabel,
|
||||||
|
}: PricingFeatureLineProps) => {
|
||||||
|
return hintContent ? (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Stack spacing={5}>
|
||||||
|
{hintLabel && (
|
||||||
|
<Text className={styles.featurePopoverLabel}>{hintLabel}</Text>
|
||||||
|
)}
|
||||||
|
<Text className={styles.featurePopoverContent}>{hintContent}</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
position={Position.TOP_LEFT}
|
||||||
|
popoverClassName={styles.featurePopover}
|
||||||
|
modifiers={{ offset: { enabled: true, offset: '0,10' } }}
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
<Group noWrap spacing={8} style={{ cursor: 'help' }}>
|
||||||
|
<CheckCircled height={12} width={12} />
|
||||||
|
<Box className={styles.featureItem}>{children}</Box>
|
||||||
|
</Group>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Group noWrap spacing={8}>
|
||||||
<CheckCircled height={12} width={12} />
|
<CheckCircled height={12} width={12} />
|
||||||
<Box className={styles.featureItem}>{children}</Box>
|
<Box className={styles.featureItem}>{children}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export const ACCOUNT_TYPE = {
|
|||||||
BANK: 'bank',
|
BANK: 'bank',
|
||||||
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
||||||
INVENTORY: 'inventory',
|
INVENTORY: 'inventory',
|
||||||
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||||
FIXED_ASSET: 'fixed-asset',
|
FIXED_ASSET: 'fixed-asset',
|
||||||
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
NON_CURRENT_ASSET: 'non-current-asset',
|
||||||
|
|
||||||
ACCOUNTS_PAYABLE: 'accounts-payable',
|
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||||
CREDIT_CARD: 'credit-card',
|
CREDIT_CARD: 'credit-card',
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ export enum DRAWERS {
|
|||||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||||
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
||||||
|
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,140 @@
|
|||||||
// @ts-nocheck
|
interface SubscriptionPlanFeature {
|
||||||
// Subscription plans.
|
text: string;
|
||||||
export const plans = [
|
hint?: string;
|
||||||
|
label?: string;
|
||||||
];
|
style?: Record<string, string>;
|
||||||
|
}
|
||||||
|
interface SubscriptionPlan {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
features: SubscriptionPlanFeature[];
|
||||||
|
featured?: boolean;
|
||||||
|
monthlyPrice: string;
|
||||||
|
monthlyPriceLabel: string;
|
||||||
|
annuallyPrice: string;
|
||||||
|
annuallyPriceLabel: string;
|
||||||
|
monthlyVariantId: string;
|
||||||
|
annuallyVariantId: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Payment methods.
|
export const SubscriptionPlans = [
|
||||||
export const paymentMethods = [
|
{
|
||||||
|
name: 'Capital Basic',
|
||||||
];
|
slug: 'capital_basic',
|
||||||
|
description: 'Good for service businesses that just started.',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
text: 'Unlimited Sale Invoices',
|
||||||
|
hintLabel: 'Unlimited Sale Invoices',
|
||||||
|
hint: 'Good for service businesses that just started for service businesses that just started',
|
||||||
|
},
|
||||||
|
{ text: 'Unlimated Sale Estimates' },
|
||||||
|
{ text: 'Track GST and VAT' },
|
||||||
|
{ text: 'Connect Banks for Automatic Importing' },
|
||||||
|
{ text: 'Chart of Accounts' },
|
||||||
|
{
|
||||||
|
text: 'Manual Journals',
|
||||||
|
hintLabel: 'Manual Journals',
|
||||||
|
hint: 'Write manual journals entries for financial transactions not automatically captured by the system to adjust financial statements.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Basic Financial Reports & Insights',
|
||||||
|
hint: 'Balance sheet, profit & loss statement, cashflow statement, general ledger, journal sheet, A/P aging summary, A/R aging summary',
|
||||||
|
},
|
||||||
|
{ text: 'Unlimited User Seats' },
|
||||||
|
],
|
||||||
|
monthlyPrice: '$10',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$7.5',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
monthlyVariantId: '446152',
|
||||||
|
// monthlyVariantId: '450016',
|
||||||
|
annuallyVariantId: '446153',
|
||||||
|
// annuallyVariantId: '450018',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Essential',
|
||||||
|
slug: 'capital_plus',
|
||||||
|
description: 'Good for have inventory and want more financial reports.',
|
||||||
|
features: [
|
||||||
|
{ text: 'All Capital Basic features' },
|
||||||
|
{ text: 'Purchase Invoices' },
|
||||||
|
{
|
||||||
|
text: 'Multi Currency Transactions',
|
||||||
|
hintLabel: 'Multi Currency',
|
||||||
|
hint: 'Pay and get paid and do manual journals in any currency with real time exchange rates conversions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Transactions Locking',
|
||||||
|
hintLabel: 'Transactions Locking',
|
||||||
|
hint: 'Transaction Locking freezes transactions to prevent any additions, modifications, or deletions of transactions recorded during the specified date.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Inventory Tracking',
|
||||||
|
hintLabel: 'Inventory Tracking',
|
||||||
|
hint: 'Track goods in the stock, cost of goods, and get notifications when quantity is low.',
|
||||||
|
},
|
||||||
|
{ text: 'Smart Financial Reports' },
|
||||||
|
{ text: 'Advanced Inventory Reports' },
|
||||||
|
],
|
||||||
|
monthlyPrice: '$20',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$15',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
// monthlyVariantId: '450028',
|
||||||
|
monthlyVariantId: '446155',
|
||||||
|
// annuallyVariantId: '450029',
|
||||||
|
annuallyVariantId: '446156',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Plus',
|
||||||
|
slug: 'essentials',
|
||||||
|
description: 'Good for business want financial and access control.',
|
||||||
|
features: [
|
||||||
|
{ text: 'All Capital Essential features' },
|
||||||
|
{ text: 'Custom User Roles Access' },
|
||||||
|
{ text: 'Vendor Credits' },
|
||||||
|
{
|
||||||
|
text: 'Budgeting',
|
||||||
|
hint: 'Create multiple budgets and compare targets with actuals to understand how your business is performing.',
|
||||||
|
},
|
||||||
|
{ text: 'Analysis Cost Center' },
|
||||||
|
],
|
||||||
|
monthlyPrice: '$25',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$19',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
featured: true,
|
||||||
|
// monthlyVariantId: '450031',
|
||||||
|
monthlyVariantId: '446165',
|
||||||
|
// annuallyVariantId: '450032',
|
||||||
|
annuallyVariantId: '446164',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Capital Big',
|
||||||
|
slug: 'essentials',
|
||||||
|
description: 'Good for businesses have multiple branches.',
|
||||||
|
features: [
|
||||||
|
{ text: 'All Capital Plus features' },
|
||||||
|
{
|
||||||
|
text: 'Multiple Branches',
|
||||||
|
hintLabel: '',
|
||||||
|
hint: 'Track the organization transactions and accounts in multiple branches.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Multiple Warehouses',
|
||||||
|
hintLabel: 'Multiple Warehouses',
|
||||||
|
hint: 'Track the organization inventory in multiple warehouses and transfer goods between them.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
monthlyPrice: '$40',
|
||||||
|
monthlyPriceLabel: 'Per month',
|
||||||
|
annuallyPrice: '$30',
|
||||||
|
annuallyPriceLabel: 'Per month',
|
||||||
|
// monthlyVariantId: '450024',
|
||||||
|
monthlyVariantId: '446167',
|
||||||
|
// annuallyVariantId: '450025',
|
||||||
|
annuallyVariantId: '446168',
|
||||||
|
},
|
||||||
|
] as SubscriptionPlan[];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components';
|
|||||||
import { transformTableStateToQuery, compose } from '@/utils';
|
import { transformTableStateToQuery, compose } from '@/utils';
|
||||||
|
|
||||||
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
|
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
|
||||||
import ManualJournalsViewTabs from './ManualJournalsViewTabs';
|
|
||||||
import ManualJournalsDataTable from './ManualJournalsDataTable';
|
import ManualJournalsDataTable from './ManualJournalsDataTable';
|
||||||
import ManualJournalsActionsBar from './ManualJournalActionsBar';
|
import ManualJournalsActionsBar from './ManualJournalActionsBar';
|
||||||
import withManualJournals from './withManualJournals';
|
import withManualJournals from './withManualJournals';
|
||||||
@@ -29,7 +28,6 @@ function ManualJournalsTable({
|
|||||||
<ManualJournalsActionsBar />
|
<ManualJournalsActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<ManualJournalsViewTabs />
|
|
||||||
<ManualJournalsDataTable />
|
<ManualJournalsDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</ManualJournalsListProvider>
|
</ManualJournalsListProvider>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import '@/style/pages/Accounts/List.scss';
|
import '@/style/pages/Accounts/List.scss';
|
||||||
import { DashboardPageContent, DashboardContentTable } from '@/components';
|
|
||||||
|
|
||||||
|
import { DashboardPageContent, DashboardContentTable } from '@/components';
|
||||||
import { AccountsChartProvider } from './AccountsChartProvider';
|
import { AccountsChartProvider } from './AccountsChartProvider';
|
||||||
import AccountsViewsTabs from './AccountsViewsTabs';
|
|
||||||
import AccountsActionsBar from './AccountsActionsBar';
|
import AccountsActionsBar from './AccountsActionsBar';
|
||||||
import AccountsDataTable from './AccountsDataTable';
|
import AccountsDataTable from './AccountsDataTable';
|
||||||
|
|
||||||
import withAccounts from '@/containers/Accounts/withAccounts';
|
import withAccounts from '@/containers/Accounts/withAccounts';
|
||||||
import withAccountsTableActions from './withAccountsTableActions';
|
import withAccountsTableActions from './withAccountsTableActions';
|
||||||
|
|
||||||
import { transformAccountsStateToQuery } from './utils';
|
import { transformAccountsStateToQuery } from './utils';
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
@@ -41,8 +41,6 @@ function AccountsChart({
|
|||||||
<AccountsActionsBar />
|
<AccountsActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<AccountsViewsTabs />
|
|
||||||
|
|
||||||
<DashboardContentTable>
|
<DashboardContentTable>
|
||||||
<AccountsDataTable />
|
<AccountsDataTable />
|
||||||
</DashboardContentTable>
|
</DashboardContentTable>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts';
|
|||||||
import TaxRatesAlerts from '@/containers/TaxRates/alerts';
|
import TaxRatesAlerts from '@/containers/TaxRates/alerts';
|
||||||
import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
|
import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
|
||||||
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
|
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
|
||||||
|
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...AccountsAlerts,
|
...AccountsAlerts,
|
||||||
@@ -56,5 +57,6 @@ export default [
|
|||||||
...ProjectAlerts,
|
...ProjectAlerts,
|
||||||
...TaxRatesAlerts,
|
...TaxRatesAlerts,
|
||||||
...CashflowAlerts,
|
...CashflowAlerts,
|
||||||
...BankRulesAlerts
|
...BankRulesAlerts,
|
||||||
|
...SubscriptionAlerts
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,13 +11,19 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Position,
|
Position,
|
||||||
|
Intent,
|
||||||
|
Tooltip,
|
||||||
|
MenuDivider,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
DashboardActionsBar,
|
DashboardActionsBar,
|
||||||
DashboardRowsHeightButton,
|
DashboardRowsHeightButton,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
|
AppToaster,
|
||||||
|
If,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
|
||||||
import { CashFlowMenuItems } from './utils';
|
import { CashFlowMenuItems } from './utils';
|
||||||
@@ -33,6 +39,13 @@ import withSettings from '@/containers/Settings/withSettings';
|
|||||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import {
|
||||||
|
useDisconnectBankAccount,
|
||||||
|
useUpdateBankAccount,
|
||||||
|
useExcludeUncategorizedTransactions,
|
||||||
|
useUnexcludeUncategorizedTransactions,
|
||||||
|
} from '@/hooks/query/bank-rules';
|
||||||
|
import { withBanking } from '../withBanking';
|
||||||
|
|
||||||
function AccountTransactionsActionsBar({
|
function AccountTransactionsActionsBar({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
@@ -43,17 +56,27 @@ function AccountTransactionsActionsBar({
|
|||||||
|
|
||||||
// #withSettingsActions
|
// #withSettingsActions
|
||||||
addSetting,
|
addSetting,
|
||||||
|
|
||||||
|
// #withBanking
|
||||||
|
uncategorizedTransationsIdsSelected,
|
||||||
|
excludedTransactionsIdsSelected,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { accountId } = useAccountTransactionsContext();
|
const { accountId, currentAccount } = useAccountTransactionsContext();
|
||||||
|
|
||||||
// Refresh cashflow infinity transactions hook.
|
// Refresh cashflow infinity transactions hook.
|
||||||
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
||||||
|
|
||||||
|
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
|
||||||
|
const { mutateAsync: updateBankAccount } = useUpdateBankAccount();
|
||||||
|
|
||||||
// Retrieves the money in/out buttons options.
|
// Retrieves the money in/out buttons options.
|
||||||
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
||||||
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
||||||
|
|
||||||
|
const isFeedsActive = !!currentAccount.is_feeds_active;
|
||||||
|
const isSyncingOwner = currentAccount.is_syncing_owner;
|
||||||
|
|
||||||
// Handle table row size change.
|
// Handle table row size change.
|
||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
addSetting('cashflowTransactions', 'tableSize', size);
|
addSetting('cashflowTransactions', 'tableSize', size);
|
||||||
@@ -82,11 +105,92 @@ function AccountTransactionsActionsBar({
|
|||||||
const handleBankRulesClick = () => {
|
const handleBankRulesClick = () => {
|
||||||
history.push(`/bank-rules?accountId=${accountId}`);
|
history.push(`/bank-rules?accountId=${accountId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handles the bank account disconnect click.
|
||||||
|
const handleDisconnectClick = () => {
|
||||||
|
disconnectBankAccount({ bankAccountId: accountId })
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The bank account has been disconnected.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// handles the bank update button click.
|
||||||
|
const handleBankUpdateClick = () => {
|
||||||
|
updateBankAccount({ bankAccountId: accountId })
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The transactions of the bank account has been updated.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
// Handle the refresh button click.
|
// Handle the refresh button click.
|
||||||
const handleRefreshBtnClick = () => {
|
const handleRefreshBtnClick = () => {
|
||||||
refresh();
|
refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: excludeUncategorizedTransactions,
|
||||||
|
isLoading: isExcludingLoading,
|
||||||
|
} = useExcludeUncategorizedTransactions();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: unexcludeUncategorizedTransactions,
|
||||||
|
isLoading: isUnexcludingLoading,
|
||||||
|
} = useUnexcludeUncategorizedTransactions();
|
||||||
|
|
||||||
|
// Handles the exclude uncategorized transactions in bulk.
|
||||||
|
const handleExcludeUncategorizedBtnClick = () => {
|
||||||
|
excludeUncategorizedTransactions({
|
||||||
|
ids: uncategorizedTransationsIdsSelected,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The selected transactions have been excluded.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the unexclude categorized button click.
|
||||||
|
const handleUnexcludeUncategorizedBtnClick = () => {
|
||||||
|
unexcludeUncategorizedTransactions({
|
||||||
|
ids: excludedTransactionsIdsSelected,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The selected excluded transactions have been unexcluded.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -129,6 +233,45 @@ function AccountTransactionsActionsBar({
|
|||||||
onChange={handleTableRowSizeChange}
|
onChange={handleTableRowSizeChange}
|
||||||
/>
|
/>
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
|
|
||||||
|
<If condition={isSyncingOwner}>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
isFeedsActive
|
||||||
|
? 'The bank syncing is active'
|
||||||
|
: 'The bank syncing is disconnected'
|
||||||
|
}
|
||||||
|
minimal={true}
|
||||||
|
position={Position.BOTTOM}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="feed" iconSize={16} />}
|
||||||
|
intent={isFeedsActive ? Intent.SUCCESS : Intent.DANGER}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
{!isEmpty(uncategorizedTransationsIdsSelected) && (
|
||||||
|
<Button
|
||||||
|
icon={<Icon icon="disable" iconSize={16} />}
|
||||||
|
text={'Exclude'}
|
||||||
|
onClick={handleExcludeUncategorizedBtnClick}
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
disabled={isExcludingLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isEmpty(excludedTransactionsIdsSelected) && (
|
||||||
|
<Button
|
||||||
|
icon={<Icon icon="disable" iconSize={16} />}
|
||||||
|
text={'Unexclude'}
|
||||||
|
onClick={handleUnexcludeUncategorizedBtnClick}
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
disabled={isUnexcludingLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</NavbarGroup>
|
</NavbarGroup>
|
||||||
|
|
||||||
<NavbarGroup align={Alignment.RIGHT}>
|
<NavbarGroup align={Alignment.RIGHT}>
|
||||||
@@ -141,7 +284,15 @@ function AccountTransactionsActionsBar({
|
|||||||
}}
|
}}
|
||||||
content={
|
content={
|
||||||
<Menu>
|
<Menu>
|
||||||
|
<If condition={isSyncingOwner && isFeedsActive}>
|
||||||
|
<MenuItem onClick={handleBankUpdateClick} text={'Update'} />
|
||||||
|
<MenuDivider />
|
||||||
|
</If>
|
||||||
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
||||||
|
|
||||||
|
<If condition={isSyncingOwner && isFeedsActive}>
|
||||||
|
<MenuItem onClick={handleDisconnectClick} text={'Disconnect'} />
|
||||||
|
</If>
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -164,4 +315,13 @@ export default compose(
|
|||||||
withSettings(({ cashflowTransactionsSettings }) => ({
|
withSettings(({ cashflowTransactionsSettings }) => ({
|
||||||
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
|
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
|
||||||
})),
|
})),
|
||||||
|
withBanking(
|
||||||
|
({
|
||||||
|
uncategorizedTransationsIdsSelected,
|
||||||
|
excludedTransactionsIdsSelected,
|
||||||
|
}) => ({
|
||||||
|
uncategorizedTransationsIdsSelected,
|
||||||
|
excludedTransactionsIdsSelected,
|
||||||
|
}),
|
||||||
|
),
|
||||||
)(AccountTransactionsActionsBar);
|
)(AccountTransactionsActionsBar);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function AccountTransactionsDataTable({
|
|||||||
|
|
||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
setUncategorizedTransactionIdForMatching,
|
setUncategorizedTransactionIdForMatching,
|
||||||
|
setUncategorizedTransactionsSelected,
|
||||||
}) {
|
}) {
|
||||||
// Retrieve table columns.
|
// Retrieve table columns.
|
||||||
const columns = useAccountUncategorizedTransactionsColumns();
|
const columns = useAccountUncategorizedTransactionsColumns();
|
||||||
@@ -73,12 +74,19 @@ function AccountTransactionsDataTable({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle selected rows change.
|
||||||
|
const handleSelectedRowsChange = (selected) => {
|
||||||
|
const _selectedIds = selected?.map((row) => row.original.id);
|
||||||
|
setUncategorizedTransactionsSelected(_selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
noInitialFetch={true}
|
noInitialFetch={true}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={uncategorizedTransactions || []}
|
data={uncategorizedTransactions || []}
|
||||||
sticky={true}
|
sticky={true}
|
||||||
|
selectionColumn={true}
|
||||||
loading={isUncategorizedTransactionsLoading}
|
loading={isUncategorizedTransactionsLoading}
|
||||||
headerLoading={isUncategorizedTransactionsLoading}
|
headerLoading={isUncategorizedTransactionsLoading}
|
||||||
expandColumnSpace={1}
|
expandColumnSpace={1}
|
||||||
@@ -99,6 +107,7 @@ function AccountTransactionsDataTable({
|
|||||||
'There is no uncategorized transactions in the current account.'
|
'There is no uncategorized transactions in the current account.'
|
||||||
}
|
}
|
||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
payload={{
|
payload={{
|
||||||
onExclude: handleExcludeTransaction,
|
onExclude: handleExcludeTransaction,
|
||||||
onCategorize: handleCategorizeBtnClick,
|
onCategorize: handleCategorizeBtnClick,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import * as R from 'ramda';
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
TableFastCell,
|
TableFastCell,
|
||||||
@@ -19,11 +19,20 @@ import { useExcludedTransactionsBoot } from './ExcludedTransactionsTableBoot';
|
|||||||
|
|
||||||
import { ActionsMenu } from './_components';
|
import { ActionsMenu } from './_components';
|
||||||
import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
import {
|
||||||
|
WithBankingActionsProps,
|
||||||
|
withBankingActions,
|
||||||
|
} from '../../withBankingActions';
|
||||||
|
|
||||||
|
interface ExcludeTransactionsTableProps extends WithBankingActionsProps {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the recognized account transactions datatable.
|
* Renders the recognized account transactions datatable.
|
||||||
*/
|
*/
|
||||||
export function ExcludedTransactionsTable() {
|
function ExcludedTransactionsTableRoot({
|
||||||
|
// #withBankingActions
|
||||||
|
setExcludedTransactionsSelected,
|
||||||
|
}: ExcludeTransactionsTableProps) {
|
||||||
const { excludedBankTransactions } = useExcludedTransactionsBoot();
|
const { excludedBankTransactions } = useExcludedTransactionsBoot();
|
||||||
const { mutateAsync: unexcludeBankTransaction } =
|
const { mutateAsync: unexcludeBankTransaction } =
|
||||||
useUnexcludeUncategorizedTransaction();
|
useUnexcludeUncategorizedTransaction();
|
||||||
@@ -55,6 +64,12 @@ export function ExcludedTransactionsTable() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle selected rows change.
|
||||||
|
const handleSelectedRowsChange = (selected) => {
|
||||||
|
const _selectedIds = selected?.map((row) => row.original.id);
|
||||||
|
setExcludedTransactionsSelected(_selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
noInitialFetch={true}
|
noInitialFetch={true}
|
||||||
@@ -80,6 +95,8 @@ export function ExcludedTransactionsTable() {
|
|||||||
onColumnResizing={handleColumnResizing}
|
onColumnResizing={handleColumnResizing}
|
||||||
noResults={'There is no excluded bank transactions.'}
|
noResults={'There is no excluded bank transactions.'}
|
||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
|
selectionColumn={true}
|
||||||
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
payload={{
|
payload={{
|
||||||
onRestore: handleRestoreClick,
|
onRestore: handleRestoreClick,
|
||||||
}}
|
}}
|
||||||
@@ -87,6 +104,10 @@ export function ExcludedTransactionsTable() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ExcludedTransactionsTable = R.compose(withBankingActions)(
|
||||||
|
ExcludedTransactionsTableRoot,
|
||||||
|
);
|
||||||
|
|
||||||
const DashboardConstrantTable = styled(DataTable)`
|
const DashboardConstrantTable = styled(DataTable)`
|
||||||
.table {
|
.table {
|
||||||
.thead {
|
.thead {
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import { ExcludedTransactionsTable } from "../ExcludedTransactions/ExcludedTransactionsTable";
|
// @ts-nocheck
|
||||||
import { ExcludedBankTransactionsTableBoot } from "../ExcludedTransactions/ExcludedTransactionsTableBoot";
|
import { useEffect } from 'react';
|
||||||
import { AccountTransactionsCard } from "./AccountTransactionsCard";
|
import * as R from 'ramda';
|
||||||
|
import {
|
||||||
|
WithBankingActionsProps,
|
||||||
|
withBankingActions,
|
||||||
|
} from '../../withBankingActions';
|
||||||
|
import { ExcludedTransactionsTable } from '../ExcludedTransactions/ExcludedTransactionsTable';
|
||||||
|
import { ExcludedBankTransactionsTableBoot } from '../ExcludedTransactions/ExcludedTransactionsTableBoot';
|
||||||
|
import { AccountTransactionsCard } from './AccountTransactionsCard';
|
||||||
|
|
||||||
|
interface AccountExcludedTransactionsProps extends WithBankingActionsProps {}
|
||||||
|
|
||||||
|
function AccountExcludedTransactionsRoot({
|
||||||
|
// #withBankingActions
|
||||||
|
resetExcludedTransactionsSelected,
|
||||||
|
}: AccountExcludedTransactionsProps) {
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
resetExcludedTransactionsSelected();
|
||||||
|
},
|
||||||
|
[resetExcludedTransactionsSelected],
|
||||||
|
);
|
||||||
|
|
||||||
export function AccountExcludedTransactions() {
|
|
||||||
return (
|
return (
|
||||||
<ExcludedBankTransactionsTableBoot>
|
<ExcludedBankTransactionsTableBoot>
|
||||||
<AccountTransactionsCard>
|
<AccountTransactionsCard>
|
||||||
@@ -11,3 +30,7 @@ export function AccountExcludedTransactions() {
|
|||||||
</ExcludedBankTransactionsTableBoot>
|
</ExcludedBankTransactionsTableBoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AccountExcludedTransactions = R.compose(withBankingActions)(
|
||||||
|
AccountExcludedTransactionsRoot,
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import AccountTransactionsUncategorizedTable from '../AccountTransactionsUncategorizedTable';
|
import AccountTransactionsUncategorizedTable from '../AccountTransactionsUncategorizedTable';
|
||||||
import { AccountUncategorizedTransactionsBoot } from '../AllTransactionsUncategorizedBoot';
|
import { AccountUncategorizedTransactionsBoot } from '../AllTransactionsUncategorizedBoot';
|
||||||
import { AccountTransactionsCard } from './AccountTransactionsCard';
|
import { AccountTransactionsCard } from './AccountTransactionsCard';
|
||||||
|
import {
|
||||||
|
WithBankingActionsProps,
|
||||||
|
withBankingActions,
|
||||||
|
} from '../../withBankingActions';
|
||||||
|
|
||||||
|
interface AccountUncategorizedTransactionsAllRootProps
|
||||||
|
extends WithBankingActionsProps {}
|
||||||
|
|
||||||
|
function AccountUncategorizedTransactionsAllRoot({
|
||||||
|
resetUncategorizedTransactionsSelected,
|
||||||
|
}: AccountUncategorizedTransactionsAllRootProps) {
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
resetUncategorizedTransactionsSelected();
|
||||||
|
},
|
||||||
|
[resetUncategorizedTransactionsSelected],
|
||||||
|
);
|
||||||
|
|
||||||
export function AccountUncategorizedTransactionsAll() {
|
|
||||||
return (
|
return (
|
||||||
<AccountUncategorizedTransactionsBoot>
|
<AccountUncategorizedTransactionsBoot>
|
||||||
<AccountTransactionsCard>
|
<AccountTransactionsCard>
|
||||||
@@ -11,3 +29,7 @@ export function AccountUncategorizedTransactionsAll() {
|
|||||||
</AccountUncategorizedTransactionsBoot>
|
</AccountUncategorizedTransactionsBoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AccountUncategorizedTransactionsAll = R.compose(
|
||||||
|
withBankingActions,
|
||||||
|
)(AccountUncategorizedTransactionsAllRoot);
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export const withBanking = (mapState) => {
|
|||||||
|
|
||||||
reconcileMatchingTransactionPendingAmount:
|
reconcileMatchingTransactionPendingAmount:
|
||||||
state.plaid.openReconcileMatchingTransaction.pending,
|
state.plaid.openReconcileMatchingTransaction.pending,
|
||||||
|
|
||||||
|
uncategorizedTransationsIdsSelected:
|
||||||
|
state.plaid.uncategorizedTransactionsSelected,
|
||||||
|
|
||||||
|
excludedTransactionsIdsSelected: state.plaid.excludedTransactionsSelected,
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import {
|
|||||||
setUncategorizedTransactionIdForMatching,
|
setUncategorizedTransactionIdForMatching,
|
||||||
openReconcileMatchingTransaction,
|
openReconcileMatchingTransaction,
|
||||||
closeReconcileMatchingTransaction,
|
closeReconcileMatchingTransaction,
|
||||||
|
setUncategorizedTransactionsSelected,
|
||||||
|
resetUncategorizedTransactionsSelected,
|
||||||
|
resetExcludedTransactionsSelected,
|
||||||
|
setExcludedTransactionsSelected,
|
||||||
} from '@/store/banking/banking.reducer';
|
} from '@/store/banking/banking.reducer';
|
||||||
|
|
||||||
export interface WithBankingActionsProps {
|
export interface WithBankingActionsProps {
|
||||||
@@ -13,6 +17,12 @@ export interface WithBankingActionsProps {
|
|||||||
) => void;
|
) => void;
|
||||||
openReconcileMatchingTransaction: (pendingAmount: number) => void;
|
openReconcileMatchingTransaction: (pendingAmount: number) => void;
|
||||||
closeReconcileMatchingTransaction: () => void;
|
closeReconcileMatchingTransaction: () => void;
|
||||||
|
|
||||||
|
setUncategorizedTransactionsSelected: (ids: Array<string | number>) => void;
|
||||||
|
resetUncategorizedTransactionsSelected: () => void;
|
||||||
|
|
||||||
|
setExcludedTransactionsSelected: (ids: Array<string | number>) => void;
|
||||||
|
resetExcludedTransactionsSelected: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
||||||
@@ -28,6 +38,40 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
|||||||
dispatch(openReconcileMatchingTransaction({ pending: pendingAmount })),
|
dispatch(openReconcileMatchingTransaction({ pending: pendingAmount })),
|
||||||
closeReconcileMatchingTransaction: () =>
|
closeReconcileMatchingTransaction: () =>
|
||||||
dispatch(closeReconcileMatchingTransaction()),
|
dispatch(closeReconcileMatchingTransaction()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected uncategorized transactions.
|
||||||
|
* @param {Array<string | number>} ids
|
||||||
|
*/
|
||||||
|
setUncategorizedTransactionsSelected: (ids: Array<string | number>) =>
|
||||||
|
dispatch(
|
||||||
|
setUncategorizedTransactionsSelected({
|
||||||
|
transactionIds: ids,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the selected uncategorized transactions.
|
||||||
|
*/
|
||||||
|
resetUncategorizedTransactionsSelected: () =>
|
||||||
|
dispatch(resetUncategorizedTransactionsSelected()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets excluded selected transactions.
|
||||||
|
* @param {Array<string | number>} ids
|
||||||
|
*/
|
||||||
|
setExcludedTransactionsSelected: (ids: Array<string | number>) =>
|
||||||
|
dispatch(
|
||||||
|
setExcludedTransactionsSelected({
|
||||||
|
ids,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the excluded selected transactions
|
||||||
|
*/
|
||||||
|
resetExcludedTransactionsSelected: () =>
|
||||||
|
dispatch(resetExcludedTransactionsSelected()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withBankingActions = connect<
|
export const withBankingActions = connect<
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import '@/style/pages/Customers/List.scss';
|
|||||||
import { DashboardPageContent } from '@/components';
|
import { DashboardPageContent } from '@/components';
|
||||||
|
|
||||||
import CustomersActionsBar from './CustomersActionsBar';
|
import CustomersActionsBar from './CustomersActionsBar';
|
||||||
import CustomersViewsTabs from './CustomersViewsTabs';
|
|
||||||
import CustomersTable from './CustomersTable';
|
import CustomersTable from './CustomersTable';
|
||||||
import { CustomersListProvider } from './CustomersListProvider';
|
import { CustomersListProvider } from './CustomersListProvider';
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@ function CustomersList({
|
|||||||
<CustomersActionsBar />
|
<CustomersActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<CustomersViewsTabs />
|
|
||||||
<CustomersTable />
|
<CustomersTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</CustomersListProvider>
|
</CustomersListProvider>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import '@/style/pages/Expense/List.scss';
|
|||||||
import { DashboardPageContent } from '@/components';
|
import { DashboardPageContent } from '@/components';
|
||||||
|
|
||||||
import ExpenseActionsBar from './ExpenseActionsBar';
|
import ExpenseActionsBar from './ExpenseActionsBar';
|
||||||
import ExpenseViewTabs from './ExpenseViewTabs';
|
|
||||||
import ExpenseDataTable from './ExpenseDataTable';
|
import ExpenseDataTable from './ExpenseDataTable';
|
||||||
|
|
||||||
import withExpenses from './withExpenses';
|
import withExpenses from './withExpenses';
|
||||||
@@ -42,7 +41,6 @@ function ExpensesList({
|
|||||||
<ExpenseActionsBar />
|
<ExpenseActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<ExpenseViewTabs />
|
|
||||||
<ExpenseDataTable />
|
<ExpenseDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</ExpensesListProvider>
|
</ExpensesListProvider>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { DashboardPageContent } from '@/components';
|
|||||||
import { ItemsListProvider } from './ItemsListProvider';
|
import { ItemsListProvider } from './ItemsListProvider';
|
||||||
|
|
||||||
import ItemsActionsBar from './ItemsActionsBar';
|
import ItemsActionsBar from './ItemsActionsBar';
|
||||||
import ItemsViewsTabs from './ItemsViewsTabs';
|
|
||||||
import ItemsDataTable from './ItemsDataTable';
|
import ItemsDataTable from './ItemsDataTable';
|
||||||
|
|
||||||
import withItems from './withItems';
|
import withItems from './withItems';
|
||||||
@@ -41,7 +40,6 @@ function ItemsList({
|
|||||||
<ItemsActionsBar />
|
<ItemsActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<ItemsViewsTabs />
|
|
||||||
<ItemsDataTable />
|
<ItemsDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</ItemsListProvider>
|
</ItemsListProvider>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import '@/style/pages/Bills/List.scss';
|
|||||||
import { BillsListProvider } from './BillsListProvider';
|
import { BillsListProvider } from './BillsListProvider';
|
||||||
|
|
||||||
import BillsActionsBar from './BillsActionsBar';
|
import BillsActionsBar from './BillsActionsBar';
|
||||||
import BillsViewsTabs from './BillsViewsTabs';
|
|
||||||
import BillsTable from './BillsTable';
|
import BillsTable from './BillsTable';
|
||||||
|
|
||||||
import withBills from './withBills';
|
import withBills from './withBills';
|
||||||
@@ -42,7 +41,6 @@ function BillsList({
|
|||||||
<BillsActionsBar />
|
<BillsActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<BillsViewsTabs />
|
|
||||||
<BillsTable />
|
<BillsTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</BillsListProvider>
|
</BillsListProvider>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import '@/style/pages/VendorsCreditNote/List.scss';
|
|||||||
|
|
||||||
import { DashboardPageContent } from '@/components';
|
import { DashboardPageContent } from '@/components';
|
||||||
import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar';
|
import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar';
|
||||||
import VendorsCreditNoteViewTabs from './VendorsCreditNoteViewTabs';
|
|
||||||
import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable';
|
import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable';
|
||||||
|
|
||||||
import withVendorsCreditNotes from './withVendorsCreditNotes';
|
import withVendorsCreditNotes from './withVendorsCreditNotes';
|
||||||
@@ -37,7 +36,6 @@ function VendorsCreditNotesList({
|
|||||||
>
|
>
|
||||||
<VendorsCreditNoteActionsBar />
|
<VendorsCreditNoteActionsBar />
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<VendorsCreditNoteViewTabs />
|
|
||||||
<VendorsCreditNoteDataTable />
|
<VendorsCreditNoteDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</VendorsCreditNoteListProvider>
|
</VendorsCreditNoteListProvider>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ExcessPaymentDialog } from './dialogs/PaymentMadeExcessDialog';
|
||||||
|
|
||||||
|
export function PaymentMadeDialogs() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExcessPaymentDialog dialogName={'payment-made-excessed-payment'} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form, FormikHelpers } from 'formik';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { sumBy, defaultTo } from 'lodash';
|
import { sumBy, defaultTo } from 'lodash';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
@@ -14,6 +14,7 @@ import PaymentMadeFloatingActions from './PaymentMadeFloatingActions';
|
|||||||
import PaymentMadeFooter from './PaymentMadeFooter';
|
import PaymentMadeFooter from './PaymentMadeFooter';
|
||||||
import PaymentMadeFormBody from './PaymentMadeFormBody';
|
import PaymentMadeFormBody from './PaymentMadeFormBody';
|
||||||
import PaymentMadeFormTopBar from './PaymentMadeFormTopBar';
|
import PaymentMadeFormTopBar from './PaymentMadeFormTopBar';
|
||||||
|
import { PaymentMadeDialogs } from './PaymentMadeDialogs';
|
||||||
|
|
||||||
import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider';
|
import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider';
|
||||||
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
|
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
|
||||||
@@ -21,6 +22,7 @@ import { compose, orderingLinesIndexes } from '@/utils';
|
|||||||
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditPaymentMadeFormSchema,
|
EditPaymentMadeFormSchema,
|
||||||
@@ -31,6 +33,7 @@ import {
|
|||||||
transformToEditForm,
|
transformToEditForm,
|
||||||
transformErrors,
|
transformErrors,
|
||||||
transformFormToRequest,
|
transformFormToRequest,
|
||||||
|
getPaymentExcessAmountFromValues,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,6 +45,9 @@ function PaymentMadeForm({
|
|||||||
|
|
||||||
// #withCurrentOrganization
|
// #withCurrentOrganization
|
||||||
organization: { base_currency },
|
organization: { base_currency },
|
||||||
|
|
||||||
|
// #withDialogActions
|
||||||
|
openDialog,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -54,6 +60,7 @@ function PaymentMadeForm({
|
|||||||
submitPayload,
|
submitPayload,
|
||||||
createPaymentMadeMutate,
|
createPaymentMadeMutate,
|
||||||
editPaymentMadeMutate,
|
editPaymentMadeMutate,
|
||||||
|
isExcessConfirmed,
|
||||||
} = usePaymentMadeFormContext();
|
} = usePaymentMadeFormContext();
|
||||||
|
|
||||||
// Form initial values.
|
// Form initial values.
|
||||||
@@ -76,13 +83,11 @@ function PaymentMadeForm({
|
|||||||
// Handle the form submit.
|
// Handle the form submit.
|
||||||
const handleSubmitForm = (
|
const handleSubmitForm = (
|
||||||
values,
|
values,
|
||||||
{ setSubmitting, resetForm, setFieldError },
|
{ setSubmitting, resetForm, setFieldError }: FormikHelpers<any>,
|
||||||
) => {
|
) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
// Total payment amount of entries.
|
|
||||||
const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
|
|
||||||
|
|
||||||
if (totalPaymentAmount <= 0) {
|
if (values.amount <= 0) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
|
message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
@@ -90,6 +95,16 @@ function PaymentMadeForm({
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const excessAmount = getPaymentExcessAmountFromValues(values);
|
||||||
|
|
||||||
|
// Show the confirmation popup if the excess amount bigger than zero and
|
||||||
|
// has not been confirmed yet.
|
||||||
|
if (excessAmount > 0 && !isExcessConfirmed) {
|
||||||
|
openDialog('payment-made-excessed-payment');
|
||||||
|
setSubmitting(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Transformes the form values to request body.
|
// Transformes the form values to request body.
|
||||||
const form = transformFormToRequest(values);
|
const form = transformFormToRequest(values);
|
||||||
|
|
||||||
@@ -119,11 +134,12 @@ function PaymentMadeForm({
|
|||||||
}
|
}
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isNewMode) {
|
if (!isNewMode) {
|
||||||
editPaymentMadeMutate([paymentMadeId, form]).then(onSaved).catch(onError);
|
return editPaymentMadeMutate([paymentMadeId, form])
|
||||||
|
.then(onSaved)
|
||||||
|
.catch(onError);
|
||||||
} else {
|
} else {
|
||||||
createPaymentMadeMutate(form).then(onSaved).catch(onError);
|
return createPaymentMadeMutate(form).then(onSaved).catch(onError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,6 +165,7 @@ function PaymentMadeForm({
|
|||||||
<PaymentMadeFormBody />
|
<PaymentMadeFormBody />
|
||||||
<PaymentMadeFooter />
|
<PaymentMadeFooter />
|
||||||
<PaymentMadeFloatingActions />
|
<PaymentMadeFloatingActions />
|
||||||
|
<PaymentMadeDialogs />
|
||||||
</PaymentMadeInnerProvider>
|
</PaymentMadeInnerProvider>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
@@ -163,4 +180,5 @@ export default compose(
|
|||||||
preferredPaymentAccount: parseInt(billPaymentSettings?.withdrawalAccount),
|
preferredPaymentAccount: parseInt(billPaymentSettings?.withdrawalAccount),
|
||||||
})),
|
})),
|
||||||
withCurrentOrganization(),
|
withCurrentOrganization(),
|
||||||
|
withDialogActions,
|
||||||
)(PaymentMadeForm);
|
)(PaymentMadeForm);
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
import {
|
import {
|
||||||
T,
|
T,
|
||||||
TotalLines,
|
TotalLines,
|
||||||
TotalLine,
|
TotalLine,
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
|
FormatNumber,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { usePaymentMadeTotals } from './utils';
|
import { usePaymentMadeExcessAmount, usePaymentMadeTotals } from './utils';
|
||||||
|
|
||||||
export function PaymentMadeFormFooterRight() {
|
export function PaymentMadeFormFooterRight() {
|
||||||
const { formattedSubtotal, formattedTotal } = usePaymentMadeTotals();
|
const { formattedSubtotal, formattedTotal } = usePaymentMadeTotals();
|
||||||
|
const excessAmount = usePaymentMadeExcessAmount();
|
||||||
|
const {
|
||||||
|
values: { currency_code: currencyCode },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaymentMadeTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<PaymentMadeTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
@@ -25,6 +31,11 @@ export function PaymentMadeFormFooterRight() {
|
|||||||
value={formattedTotal}
|
value={formattedTotal}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
|
<TotalLine
|
||||||
|
title={'Excess Amount'}
|
||||||
|
value={<FormatNumber value={excessAmount} currency={currencyCode} />}
|
||||||
|
textStyle={TotalLineTextStyle.Regular}
|
||||||
|
/>
|
||||||
</PaymentMadeTotalLines>
|
</PaymentMadeTotalLines>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { sumBy } from 'lodash';
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { Money, FormattedMessage as T } from '@/components';
|
import { Money, FormattedMessage as T } from '@/components';
|
||||||
|
|
||||||
import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields';
|
import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields';
|
||||||
|
import { usePaymentmadeTotalAmount } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payment made header form.
|
* Payment made header form.
|
||||||
@@ -14,11 +14,10 @@ import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields';
|
|||||||
function PaymentMadeFormHeader() {
|
function PaymentMadeFormHeader() {
|
||||||
// Formik form context.
|
// Formik form context.
|
||||||
const {
|
const {
|
||||||
values: { entries, currency_code },
|
values: { currency_code },
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
|
|
||||||
// Calculate the payment amount of the entries.
|
const totalAmount = usePaymentmadeTotalAmount();
|
||||||
const amountPaid = useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
|
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
|
||||||
@@ -30,8 +29,9 @@ function PaymentMadeFormHeader() {
|
|||||||
<span class="big-amount__label">
|
<span class="big-amount__label">
|
||||||
<T id={'amount_received'} />
|
<T id={'amount_received'} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h1 class="big-amount__number">
|
<h1 class="big-amount__number">
|
||||||
<Money amount={amountPaid} currency={currency_code} />
|
<Money amount={totalAmount} currency={currency_code} />
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { isEmpty, toSafeInteger } from 'lodash';
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
import { DateInput } from '@blueprintjs/datetime';
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
|
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
|
||||||
import { FormattedMessage as T, VendorsSelect } from '@/components';
|
import { FormattedMessage as T, VendorsSelect } from '@/components';
|
||||||
import { toSafeInteger } from 'lodash';
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -68,7 +68,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
|
|||||||
const fullAmount = safeSumBy(newEntries, 'payment_amount');
|
const fullAmount = safeSumBy(newEntries, 'payment_amount');
|
||||||
|
|
||||||
setFieldValue('entries', newEntries);
|
setFieldValue('entries', newEntries);
|
||||||
setFieldValue('full_amount', fullAmount);
|
setFieldValue('amount', fullAmount);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles the full-amount field blur.
|
// Handles the full-amount field blur.
|
||||||
@@ -115,10 +115,10 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
|
|||||||
</FastField>
|
</FastField>
|
||||||
|
|
||||||
{/* ------------ Full amount ------------ */}
|
{/* ------------ Full amount ------------ */}
|
||||||
<Field name={'full_amount'}>
|
<Field name={'amount'}>
|
||||||
{({
|
{({
|
||||||
form: {
|
form: {
|
||||||
values: { currency_code },
|
values: { currency_code, entries },
|
||||||
},
|
},
|
||||||
field: { value },
|
field: { value },
|
||||||
meta: { error, touched },
|
meta: { error, touched },
|
||||||
@@ -129,28 +129,30 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
|
|||||||
className={('form-group--full-amount', Classes.FILL)}
|
className={('form-group--full-amount', Classes.FILL)}
|
||||||
intent={inputIntent({ error, touched })}
|
intent={inputIntent({ error, touched })}
|
||||||
labelInfo={<Hint />}
|
labelInfo={<Hint />}
|
||||||
helperText={<ErrorMessage name="full_amount" />}
|
helperText={<ErrorMessage name="amount" />}
|
||||||
>
|
>
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
<InputPrependText text={currency_code} />
|
<InputPrependText text={currency_code} />
|
||||||
<MoneyInputGroup
|
<MoneyInputGroup
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setFieldValue('full_amount', value);
|
setFieldValue('amount', value);
|
||||||
}}
|
}}
|
||||||
onBlurValue={onFullAmountBlur}
|
onBlurValue={onFullAmountBlur}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
|
|
||||||
<Button
|
{!isEmpty(entries) && (
|
||||||
onClick={handleReceiveFullAmountClick}
|
<Button
|
||||||
className={'receive-full-amount'}
|
onClick={handleReceiveFullAmountClick}
|
||||||
small={true}
|
className={'receive-full-amount'}
|
||||||
minimal={true}
|
small={true}
|
||||||
>
|
minimal={true}
|
||||||
<T id={'receive_full_amount'} /> (
|
>
|
||||||
<Money amount={payableFullAmount} currency={currency_code} />)
|
<T id={'receive_full_amount'} /> (
|
||||||
</Button>
|
<Money amount={payableFullAmount} currency={currency_code} />)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
import { useFeatureCan } from '@/hooks/state';
|
import { useFeatureCan } from '@/hooks/state';
|
||||||
import {
|
import {
|
||||||
@@ -71,6 +71,8 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
|
|||||||
|
|
||||||
const isFeatureLoading = isBranchesLoading;
|
const isFeatureLoading = isBranchesLoading;
|
||||||
|
|
||||||
|
const [isExcessConfirmed, setIsExcessConfirmed] = useState<boolean>(false);
|
||||||
|
|
||||||
// Provider payload.
|
// Provider payload.
|
||||||
const provider = {
|
const provider = {
|
||||||
paymentMadeId,
|
paymentMadeId,
|
||||||
@@ -98,6 +100,9 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
|
|||||||
|
|
||||||
setSubmitPayload,
|
setSubmitPayload,
|
||||||
setPaymentVendorId,
|
setPaymentVendorId,
|
||||||
|
|
||||||
|
isExcessConfirmed,
|
||||||
|
setIsExcessConfirmed,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogSuspense } from '@/components';
|
||||||
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const ExcessPaymentDialogContent = React.lazy(() =>
|
||||||
|
import('./PaymentMadeExcessDialogContent').then((module) => ({
|
||||||
|
default: module.ExcessPaymentDialogContent,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exess payment dialog of the payment made form.
|
||||||
|
*/
|
||||||
|
function ExcessPaymentDialogRoot({ dialogName, isOpen }) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
name={dialogName}
|
||||||
|
title={'Excess Payment'}
|
||||||
|
isOpen={isOpen}
|
||||||
|
canEscapeJeyClose={true}
|
||||||
|
autoFocus={true}
|
||||||
|
style={{ width: 500 }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<ExcessPaymentDialogContent dialogName={dialogName} />
|
||||||
|
</DialogSuspense>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExcessPaymentDialog = compose(withDialogRedux())(
|
||||||
|
ExcessPaymentDialogRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
ExcessPaymentDialog.displayName = 'ExcessPaymentDialog';
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Classes, Intent } from '@blueprintjs/core';
|
||||||
|
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
|
import { FormatNumber } from '@/components';
|
||||||
|
import { usePaymentMadeFormContext } from '../../PaymentMadeFormProvider';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { usePaymentMadeExcessAmount } from '../../utils';
|
||||||
|
|
||||||
|
interface ExcessPaymentValues {}
|
||||||
|
function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) {
|
||||||
|
const {
|
||||||
|
submitForm,
|
||||||
|
values: { currency_code: currencyCode },
|
||||||
|
} = useFormikContext();
|
||||||
|
const { setIsExcessConfirmed } = usePaymentMadeFormContext();
|
||||||
|
|
||||||
|
// Handles the form submitting.
|
||||||
|
const handleSubmit = (
|
||||||
|
values: ExcessPaymentValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<ExcessPaymentValues>,
|
||||||
|
) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
setIsExcessConfirmed(true);
|
||||||
|
|
||||||
|
return submitForm().then(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
closeDialog(dialogName);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// Handle close button click.
|
||||||
|
const handleCloseBtn = () => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
};
|
||||||
|
const excessAmount = usePaymentMadeExcessAmount();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik initialValues={{}} onSubmit={handleSubmit}>
|
||||||
|
<Form>
|
||||||
|
<ExcessPaymentDialogContentForm
|
||||||
|
excessAmount={
|
||||||
|
<FormatNumber value={excessAmount} currency={currencyCode} />
|
||||||
|
}
|
||||||
|
onClose={handleCloseBtn}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExcessPaymentDialogContent = R.compose(withDialogActions)(
|
||||||
|
ExcessPaymentDialogContentRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ExcessPaymentDialogContentFormProps {
|
||||||
|
excessAmount: string | number | React.ReactNode;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExcessPaymentDialogContentForm({
|
||||||
|
excessAmount,
|
||||||
|
onClose,
|
||||||
|
}: ExcessPaymentDialogContentFormProps) {
|
||||||
|
const { submitForm, isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
const handleCloseBtn = () => {
|
||||||
|
onClose && onClose();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
<p style={{ marginBottom: 20 }}>
|
||||||
|
Would you like to record the excess amount of{' '}
|
||||||
|
<strong>{excessAmount}</strong> as credit payment from the vendor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => submitForm()}
|
||||||
|
>
|
||||||
|
Save Payment as Credit
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCloseBtn}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './PaymentMadeExcessDialog';
|
||||||
@@ -37,7 +37,7 @@ export const defaultPaymentMadeEntry = {
|
|||||||
|
|
||||||
// Default initial values of payment made.
|
// Default initial values of payment made.
|
||||||
export const defaultPaymentMade = {
|
export const defaultPaymentMade = {
|
||||||
full_amount: '',
|
amount: '',
|
||||||
vendor_id: '',
|
vendor_id: '',
|
||||||
payment_account_id: '',
|
payment_account_id: '',
|
||||||
payment_date: moment(new Date()).format('YYYY-MM-DD'),
|
payment_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
@@ -53,10 +53,10 @@ export const defaultPaymentMade = {
|
|||||||
|
|
||||||
export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
|
export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
|
||||||
const attachments = transformAttachmentsToForm(paymentMade);
|
const attachments = transformAttachmentsToForm(paymentMade);
|
||||||
|
const appliedAmount = safeSumBy(paymentMadeEntries, 'payment_amount');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...transformToForm(paymentMade, defaultPaymentMade),
|
...transformToForm(paymentMade, defaultPaymentMade),
|
||||||
full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'),
|
|
||||||
entries: [
|
entries: [
|
||||||
...paymentMadeEntries.map((paymentMadeEntry) => ({
|
...paymentMadeEntries.map((paymentMadeEntry) => ({
|
||||||
...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry),
|
...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry),
|
||||||
@@ -177,6 +177,30 @@ export const usePaymentMadeTotals = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const usePaymentmadeTotalAmount = () => {
|
||||||
|
const {
|
||||||
|
values: { amount },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaymentMadeAppliedAmount = () => {
|
||||||
|
const {
|
||||||
|
values: { entries },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
// Retrieves the invoice entries total.
|
||||||
|
return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaymentMadeExcessAmount = () => {
|
||||||
|
const appliedAmount = usePaymentMadeAppliedAmount();
|
||||||
|
const totalAmount = usePaymentmadeTotalAmount();
|
||||||
|
|
||||||
|
return Math.abs(totalAmount - appliedAmount);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detarmines whether the bill has foreign customer.
|
* Detarmines whether the bill has foreign customer.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -191,3 +215,10 @@ export const usePaymentMadeIsForeignCustomer = () => {
|
|||||||
);
|
);
|
||||||
return isForeignCustomer;
|
return isForeignCustomer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPaymentExcessAmountFromValues = (values) => {
|
||||||
|
const appliedAmount = sumBy(values.entries, 'payment_amount');
|
||||||
|
const totalAmount = values.amount;
|
||||||
|
|
||||||
|
return Math.abs(totalAmount - appliedAmount);
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components';
|
|||||||
import { PaymentMadesListProvider } from './PaymentMadesListProvider';
|
import { PaymentMadesListProvider } from './PaymentMadesListProvider';
|
||||||
import PaymentMadeActionsBar from './PaymentMadeActionsBar';
|
import PaymentMadeActionsBar from './PaymentMadeActionsBar';
|
||||||
import PaymentMadesTable from './PaymentMadesTable';
|
import PaymentMadesTable from './PaymentMadesTable';
|
||||||
import PaymentMadeViewTabs from './PaymentMadeViewTabs';
|
|
||||||
|
|
||||||
import withPaymentMades from './withPaymentMade';
|
import withPaymentMades from './withPaymentMade';
|
||||||
import withPaymentMadeActions from './withPaymentMadeActions';
|
import withPaymentMadeActions from './withPaymentMadeActions';
|
||||||
@@ -41,7 +40,6 @@ function PaymentMadeList({
|
|||||||
<PaymentMadeActionsBar />
|
<PaymentMadeActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<PaymentMadeViewTabs />
|
|
||||||
<PaymentMadesTable />
|
<PaymentMadesTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</PaymentMadesListProvider>
|
</PaymentMadesListProvider>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import '@/style/pages/CreditNote/List.scss';
|
|||||||
|
|
||||||
import { DashboardPageContent } from '@/components';
|
import { DashboardPageContent } from '@/components';
|
||||||
import CreditNotesActionsBar from './CreditNotesActionsBar';
|
import CreditNotesActionsBar from './CreditNotesActionsBar';
|
||||||
import CreditNotesViewTabs from './CreditNotesViewTabs';
|
|
||||||
import CreditNotesDataTable from './CreditNotesDataTable';
|
import CreditNotesDataTable from './CreditNotesDataTable';
|
||||||
|
|
||||||
import withCreditNotes from './withCreditNotes';
|
import withCreditNotes from './withCreditNotes';
|
||||||
@@ -36,8 +35,8 @@ function CreditNotesList({
|
|||||||
tableStateChanged={creditNoteTableStateChanged}
|
tableStateChanged={creditNoteTableStateChanged}
|
||||||
>
|
>
|
||||||
<CreditNotesActionsBar />
|
<CreditNotesActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<CreditNotesViewTabs />
|
|
||||||
<CreditNotesDataTable />
|
<CreditNotesDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</CreditNotesListProvider>
|
</CreditNotesListProvider>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DashboardContentTable, DashboardPageContent } from '@/components';
|
import { DashboardPageContent } from '@/components';
|
||||||
|
|
||||||
import '@/style/pages/SaleEstimate/List.scss';
|
import '@/style/pages/SaleEstimate/List.scss';
|
||||||
|
|
||||||
import EstimatesActionsBar from './EstimatesActionsBar';
|
import EstimatesActionsBar from './EstimatesActionsBar';
|
||||||
import EstimatesViewTabs from './EstimatesViewTabs';
|
|
||||||
import EstimatesDataTable from './EstimatesDataTable';
|
import EstimatesDataTable from './EstimatesDataTable';
|
||||||
|
|
||||||
import withEstimates from './withEstimates';
|
import withEstimates from './withEstimates';
|
||||||
@@ -41,7 +40,6 @@ function EstimatesList({
|
|||||||
<EstimatesActionsBar />
|
<EstimatesActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<EstimatesViewTabs />
|
|
||||||
<EstimatesDataTable />
|
<EstimatesDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</EstimatesListProvider>
|
</EstimatesListProvider>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import '@/style/pages/SaleInvoice/List.scss';
|
|||||||
import { DashboardPageContent } from '@/components';
|
import { DashboardPageContent } from '@/components';
|
||||||
import { InvoicesListProvider } from './InvoicesListProvider';
|
import { InvoicesListProvider } from './InvoicesListProvider';
|
||||||
|
|
||||||
import InvoiceViewTabs from './InvoiceViewTabs';
|
|
||||||
import InvoicesDataTable from './InvoicesDataTable';
|
import InvoicesDataTable from './InvoicesDataTable';
|
||||||
import InvoicesActionsBar from './InvoicesActionsBar';
|
import InvoicesActionsBar from './InvoicesActionsBar';
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ function InvoicesList({
|
|||||||
<InvoicesActionsBar />
|
<InvoicesActionsBar />
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<InvoiceViewTabs />
|
|
||||||
<InvoicesDataTable />
|
<InvoicesDataTable />
|
||||||
</DashboardPageContent>
|
</DashboardPageContent>
|
||||||
</InvoicesListProvider>
|
</InvoicesListProvider>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useRef } from 'react';
|
||||||
import { sumBy, isEmpty, defaultTo } from 'lodash';
|
import { sumBy, isEmpty, defaultTo } from 'lodash';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -21,6 +21,7 @@ import { PaymentReceiveInnerProvider } from './PaymentReceiveInnerProvider';
|
|||||||
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditPaymentReceiveFormSchema,
|
EditPaymentReceiveFormSchema,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
transformFormToRequest,
|
transformFormToRequest,
|
||||||
transformErrors,
|
transformErrors,
|
||||||
resetFormState,
|
resetFormState,
|
||||||
|
getExceededAmountFromValues,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { PaymentReceiveSyncIncrementSettingsToForm } from './components';
|
import { PaymentReceiveSyncIncrementSettingsToForm } from './components';
|
||||||
|
|
||||||
@@ -51,6 +53,9 @@ function PaymentReceiveForm({
|
|||||||
|
|
||||||
// #withCurrentOrganization
|
// #withCurrentOrganization
|
||||||
organization: { base_currency },
|
organization: { base_currency },
|
||||||
|
|
||||||
|
// #withDialogActions
|
||||||
|
openDialog,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -63,6 +68,7 @@ function PaymentReceiveForm({
|
|||||||
submitPayload,
|
submitPayload,
|
||||||
editPaymentReceiveMutate,
|
editPaymentReceiveMutate,
|
||||||
createPaymentReceiveMutate,
|
createPaymentReceiveMutate,
|
||||||
|
isExcessConfirmed,
|
||||||
} = usePaymentReceiveFormContext();
|
} = usePaymentReceiveFormContext();
|
||||||
|
|
||||||
// Payment receive number.
|
// Payment receive number.
|
||||||
@@ -94,18 +100,16 @@ function PaymentReceiveForm({
|
|||||||
preferredDepositAccount,
|
preferredDepositAccount,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle form submit.
|
// Handle form submit.
|
||||||
const handleSubmitForm = (
|
const handleSubmitForm = (
|
||||||
values,
|
values,
|
||||||
{ setSubmitting, resetForm, setFieldError },
|
{ setSubmitting, resetForm, setFieldError },
|
||||||
) => {
|
) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
const exceededAmount = getExceededAmountFromValues(values);
|
||||||
|
|
||||||
// Calculates the total payment amount of entries.
|
// Validates the amount should be bigger than zero.
|
||||||
const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
|
if (values.amount <= 0) {
|
||||||
|
|
||||||
if (totalPaymentAmount <= 0) {
|
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
|
message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
@@ -113,6 +117,13 @@ function PaymentReceiveForm({
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Show the confirm popup if the excessed amount bigger than zero and
|
||||||
|
// excess confirmation has not been confirmed yet.
|
||||||
|
if (exceededAmount > 0 && !isExcessConfirmed) {
|
||||||
|
setSubmitting(false);
|
||||||
|
openDialog('payment-received-excessed-payment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Transformes the form values to request body.
|
// Transformes the form values to request body.
|
||||||
const form = transformFormToRequest(values);
|
const form = transformFormToRequest(values);
|
||||||
|
|
||||||
@@ -148,11 +159,11 @@ function PaymentReceiveForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (paymentReceiveId) {
|
if (paymentReceiveId) {
|
||||||
editPaymentReceiveMutate([paymentReceiveId, form])
|
return editPaymentReceiveMutate([paymentReceiveId, form])
|
||||||
.then(onSaved)
|
.then(onSaved)
|
||||||
.catch(onError);
|
.catch(onError);
|
||||||
} else {
|
} else {
|
||||||
createPaymentReceiveMutate(form).then(onSaved).catch(onError);
|
return createPaymentReceiveMutate(form).then(onSaved).catch(onError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -202,4 +213,5 @@ export default compose(
|
|||||||
preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount,
|
preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount,
|
||||||
})),
|
})),
|
||||||
withCurrentOrganization(),
|
withCurrentOrganization(),
|
||||||
|
withDialogActions,
|
||||||
)(PaymentReceiveForm);
|
)(PaymentReceiveForm);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog';
|
import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog';
|
||||||
|
import { ExcessPaymentDialog } from './dialogs/ExcessPaymentDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payment receive form dialogs.
|
* Payment receive form dialogs.
|
||||||
@@ -21,9 +22,12 @@ export default function PaymentReceiveFormDialogs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaymentReceiveNumberDialog
|
<>
|
||||||
dialogName={'payment-receive-number-form'}
|
<PaymentReceiveNumberDialog
|
||||||
onConfirm={handleUpdatePaymentNumber}
|
dialogName={'payment-receive-number-form'}
|
||||||
/>
|
onConfirm={handleUpdatePaymentNumber}
|
||||||
|
/>
|
||||||
|
<ExcessPaymentDialog dialogName={'payment-received-excessed-payment'} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ import {
|
|||||||
TotalLine,
|
TotalLine,
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
|
FormatNumber,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { usePaymentReceiveTotals } from './utils';
|
import {
|
||||||
|
usePaymentReceiveTotals,
|
||||||
|
usePaymentReceivedTotalExceededAmount,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
export function PaymentReceiveFormFootetRight() {
|
export function PaymentReceiveFormFootetRight() {
|
||||||
const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals();
|
const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals();
|
||||||
|
const exceededAmount = usePaymentReceivedTotalExceededAmount();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaymentReceiveTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<PaymentReceiveTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
@@ -25,6 +30,11 @@ export function PaymentReceiveFormFootetRight() {
|
|||||||
value={formattedTotal}
|
value={formattedTotal}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
|
<TotalLine
|
||||||
|
title={'Exceeded Amount'}
|
||||||
|
value={<FormatNumber value={exceededAmount} />}
|
||||||
|
textStyle={TotalLineTextStyle.Regular}
|
||||||
|
/>
|
||||||
</PaymentReceiveTotalLines>
|
</PaymentReceiveTotalLines>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,15 +30,9 @@ function PaymentReceiveFormHeader() {
|
|||||||
function PaymentReceiveFormBigTotal() {
|
function PaymentReceiveFormBigTotal() {
|
||||||
// Formik form context.
|
// Formik form context.
|
||||||
const {
|
const {
|
||||||
values: { currency_code, entries },
|
values: { currency_code, amount },
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
|
|
||||||
// Calculates the total payment amount from due amount.
|
|
||||||
const paymentFullAmount = useMemo(
|
|
||||||
() => sumBy(entries, 'payment_amount'),
|
|
||||||
[entries],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
|
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
|
||||||
<div class="big-amount">
|
<div class="big-amount">
|
||||||
@@ -46,7 +40,7 @@ function PaymentReceiveFormBigTotal() {
|
|||||||
<T id={'amount_received'} />
|
<T id={'amount_received'} />
|
||||||
</span>
|
</span>
|
||||||
<h1 class="big-amount__number">
|
<h1 class="big-amount__number">
|
||||||
<Money amount={paymentFullAmount} currency={currency_code} />
|
<Money amount={amount} currency={currency_code} />
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
import { useFeatureCan } from '@/hooks/state';
|
import { useFeatureCan } from '@/hooks/state';
|
||||||
import { DashboardInsider } from '@/components';
|
import { DashboardInsider } from '@/components';
|
||||||
@@ -74,6 +74,8 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
|
|||||||
const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive();
|
const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive();
|
||||||
const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive();
|
const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive();
|
||||||
|
|
||||||
|
const [isExcessConfirmed, setIsExcessConfirmed] = useState<boolean>(false);
|
||||||
|
|
||||||
// Provider payload.
|
// Provider payload.
|
||||||
const provider = {
|
const provider = {
|
||||||
paymentReceiveId,
|
paymentReceiveId,
|
||||||
@@ -97,6 +99,9 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
|
|||||||
|
|
||||||
editPaymentReceiveMutate,
|
editPaymentReceiveMutate,
|
||||||
createPaymentReceiveMutate,
|
createPaymentReceiveMutate,
|
||||||
|
|
||||||
|
isExcessConfirmed,
|
||||||
|
setIsExcessConfirmed,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user