mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b93cb546f4 | ||
|
|
6d17f9cbeb | ||
|
|
fe214b1b2d | ||
|
|
6b6b73b77c | ||
|
|
107a6f793b | ||
|
|
67d155759e | ||
|
|
7e2e87256f | ||
|
|
df7790d7c1 | ||
|
|
72128a72c4 | ||
|
|
eb3f23554f | ||
|
|
69ddf43b3e |
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -237,4 +237,8 @@ module.exports = {
|
|||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loops: {
|
||||||
|
apiKey: process.env.LOOPS_API_KEY,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ 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 { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -274,5 +275,8 @@ export const susbcribers = () => {
|
|||||||
|
|
||||||
// Plaid
|
// Plaid
|
||||||
RecognizeSyncedBankTranasctions,
|
RecognizeSyncedBankTranasctions,
|
||||||
|
|
||||||
|
// Loops
|
||||||
|
LoopsEventsSubscriber
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,6 +4,7 @@ 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.
|
||||||
@@ -12,4 +13,8 @@ export default class TenantRepository extends CachableRepository {
|
|||||||
constructor(knex, cache, i18n) {
|
constructor(knex, cache, i18n) {
|
||||||
super(knex, cache, i18n);
|
super(knex, cache, i18n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTenantId(tenantId: number) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -73,8 +73,6 @@ export class PlaidUpdateTransactions {
|
|||||||
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,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class CreateUncategorizedTransaction {
|
|||||||
tenantId,
|
tenantId,
|
||||||
async (trx: Knex.Transaction) => {
|
async (trx: Knex.Transaction) => {
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.cashflow.onTransactionUncategorizedCreated,
|
events.cashflow.onTransactionUncategorizedCreating,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
createUncategorizedTransactionDTO,
|
createUncategorizedTransactionDTO,
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
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,
|
||||||
|
|||||||
@@ -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_')) {
|
||||||
|
|||||||
@@ -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,13 @@ export default {
|
|||||||
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User subscription events.
|
||||||
|
*/
|
||||||
|
subscription: {
|
||||||
|
onSubscribed: 'onOrganizationSubscribed',
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenants managment service.
|
* Tenants managment service.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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) {};
|
||||||
@@ -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,9 +48,9 @@
|
|||||||
}
|
}
|
||||||
.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{
|
||||||
@@ -57,3 +58,21 @@
|
|||||||
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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
];
|
export const SubscriptionPlans = [
|
||||||
|
{
|
||||||
// Payment methods.
|
name: 'Capital Basic',
|
||||||
export const paymentMethods = [
|
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[];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { DateInput } from '@blueprintjs/datetime';
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
import { toSafeInteger } from 'lodash';
|
import { isEmpty, toSafeInteger } from 'lodash';
|
||||||
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
|
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -124,11 +124,11 @@ export default function PaymentReceiveHeaderFields() {
|
|||||||
</FastField>
|
</FastField>
|
||||||
|
|
||||||
{/* ------------ Full amount ------------ */}
|
{/* ------------ Full amount ------------ */}
|
||||||
<Field name={'full_amount'}>
|
<Field name={'amount'}>
|
||||||
{({
|
{({
|
||||||
form: {
|
form: {
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
values: { currency_code },
|
values: { currency_code, entries },
|
||||||
},
|
},
|
||||||
field: { value, onChange },
|
field: { value, onChange },
|
||||||
meta: { error, touched },
|
meta: { error, touched },
|
||||||
@@ -146,21 +146,23 @@ export default function PaymentReceiveHeaderFields() {
|
|||||||
<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={totalDueAmount} currency={currency_code} />)
|
<T id={'receive_full_amount'} /> (
|
||||||
</Button>
|
<Money amount={totalDueAmount} currency={currency_code} />)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -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('./ExcessPaymentDialogContent').then((module) => ({
|
||||||
|
default: module.ExcessPaymentDialogContent,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excess payment dialog of the payment received 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,86 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Button, Classes, Intent } from '@blueprintjs/core';
|
||||||
|
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
|
import { FormatNumber } from '@/components';
|
||||||
|
import { usePaymentReceiveFormContext } from '../../PaymentReceiveFormProvider';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { usePaymentReceivedTotalExceededAmount } from '../../utils';
|
||||||
|
|
||||||
|
interface ExcessPaymentValues {}
|
||||||
|
|
||||||
|
export function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) {
|
||||||
|
const {
|
||||||
|
submitForm,
|
||||||
|
values: { currency_code: currencyCode },
|
||||||
|
} = useFormikContext();
|
||||||
|
const { setIsExcessConfirmed } = usePaymentReceiveFormContext();
|
||||||
|
const exceededAmount = usePaymentReceivedTotalExceededAmount();
|
||||||
|
|
||||||
|
const handleSubmit = (
|
||||||
|
values: ExcessPaymentValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<ExcessPaymentValues>,
|
||||||
|
) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
setIsExcessConfirmed(true);
|
||||||
|
|
||||||
|
submitForm().then(() => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik initialValues={{}} onSubmit={handleSubmit}>
|
||||||
|
<Form>
|
||||||
|
<ExcessPaymentDialogContentForm
|
||||||
|
exceededAmount={
|
||||||
|
<FormatNumber value={exceededAmount} currency={currencyCode} />
|
||||||
|
}
|
||||||
|
onClose={handleClose}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExcessPaymentDialogContent = R.compose(withDialogActions)(
|
||||||
|
ExcessPaymentDialogContentRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
function ExcessPaymentDialogContentForm({ onClose, exceededAmount }) {
|
||||||
|
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>{exceededAmount}</strong> as credit payment from the customer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={() => submitForm()}
|
||||||
|
>
|
||||||
|
Save Payment as Credit
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCloseBtn}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ExcessPaymentDialog';
|
||||||
@@ -42,12 +42,12 @@ export const defaultPaymentReceive = {
|
|||||||
// Holds the payment number that entered manually only.
|
// Holds the payment number that entered manually only.
|
||||||
payment_receive_no_manually: '',
|
payment_receive_no_manually: '',
|
||||||
statement: '',
|
statement: '',
|
||||||
full_amount: '',
|
amount: '',
|
||||||
currency_code: '',
|
currency_code: '',
|
||||||
branch_id: '',
|
branch_id: '',
|
||||||
exchange_rate: 1,
|
exchange_rate: 1,
|
||||||
entries: [],
|
entries: [],
|
||||||
attachments: []
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultRequestPaymentEntry = {
|
export const defaultRequestPaymentEntry = {
|
||||||
@@ -249,6 +249,30 @@ export const usePaymentReceiveTotals = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const usePaymentReceivedTotalAppliedAmount = () => {
|
||||||
|
const {
|
||||||
|
values: { entries },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
// Retrieves the invoice entries total.
|
||||||
|
return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaymentReceivedTotalAmount = () => {
|
||||||
|
const {
|
||||||
|
values: { amount },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaymentReceivedTotalExceededAmount = () => {
|
||||||
|
const totalAmount = usePaymentReceivedTotalAmount();
|
||||||
|
const totalApplied = usePaymentReceivedTotalAppliedAmount();
|
||||||
|
|
||||||
|
return Math.abs(totalAmount - totalApplied);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detarmines whether the payment has foreign customer.
|
* Detarmines whether the payment has foreign customer.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -273,3 +297,10 @@ export const resetFormState = ({ initialValues, values, resetForm }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getExceededAmountFromValues = (values) => {
|
||||||
|
const totalApplied = sumBy(values.entries, 'payment_amount');
|
||||||
|
const totalAmount = values.amount;
|
||||||
|
|
||||||
|
return totalAmount - totalApplied;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,3 +3,7 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.periodSwitch {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -1,32 +1,65 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { AppToaster, Group, T } from '@/components';
|
|
||||||
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
|
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
|
||||||
import { PricingPlan } from '@/components/PricingPlan/PricingPlan';
|
import { PricingPlan } from '@/components/PricingPlan/PricingPlan';
|
||||||
|
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
|
||||||
|
import {
|
||||||
|
WithPlansProps,
|
||||||
|
withPlans,
|
||||||
|
} from '@/containers/Subscriptions/withPlans';
|
||||||
|
|
||||||
|
interface SubscriptionPricingFeature {
|
||||||
|
text: string;
|
||||||
|
hint?: string;
|
||||||
|
hintLabel?: string;
|
||||||
|
style?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
interface SubscriptionPricingProps {
|
interface SubscriptionPricingProps {
|
||||||
slug: string;
|
slug: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
features?: Array<String>;
|
features?: Array<SubscriptionPricingFeature>;
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
price: string;
|
monthlyPrice: string;
|
||||||
pricePeriod: string;
|
monthlyPriceLabel: string;
|
||||||
|
annuallyPrice: string;
|
||||||
|
annuallyPriceLabel: string;
|
||||||
|
monthlyVariantId?: string;
|
||||||
|
annuallyVariantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubscriptionPricing({
|
interface SubscriptionPricingCombinedProps
|
||||||
featured,
|
extends SubscriptionPricingProps,
|
||||||
|
WithPlansProps {}
|
||||||
|
|
||||||
|
function SubscriptionPlanRoot({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
|
featured,
|
||||||
features,
|
features,
|
||||||
price,
|
monthlyPrice,
|
||||||
pricePeriod,
|
monthlyPriceLabel,
|
||||||
}: SubscriptionPricingProps) {
|
annuallyPrice,
|
||||||
|
annuallyPriceLabel,
|
||||||
|
monthlyVariantId,
|
||||||
|
annuallyVariantId,
|
||||||
|
|
||||||
|
// #withPlans
|
||||||
|
plansPeriod,
|
||||||
|
}: SubscriptionPricingCombinedProps) {
|
||||||
const { mutateAsync: getLemonCheckout, isLoading } =
|
const { mutateAsync: getLemonCheckout, isLoading } =
|
||||||
useGetLemonSqueezyCheckout();
|
useGetLemonSqueezyCheckout();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
getLemonCheckout({ variantId: '338516' })
|
const variantId =
|
||||||
|
SubscriptionPlansPeriod.Monthly === plansPeriod
|
||||||
|
? monthlyVariantId
|
||||||
|
: annuallyVariantId;
|
||||||
|
|
||||||
|
getLemonCheckout({ variantId })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const checkoutUrl = res.data.data.attributes.url;
|
const checkoutUrl = res.data.data.attributes.url;
|
||||||
window.LemonSqueezy.Url.Open(checkoutUrl);
|
window.LemonSqueezy.Url.Open(checkoutUrl);
|
||||||
@@ -42,37 +75,34 @@ function SubscriptionPricing({
|
|||||||
return (
|
return (
|
||||||
<PricingPlan featured={featured}>
|
<PricingPlan featured={featured}>
|
||||||
{featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>}
|
{featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>}
|
||||||
|
|
||||||
<PricingPlan.Header label={label} description={description} />
|
<PricingPlan.Header label={label} description={description} />
|
||||||
<PricingPlan.Price price={price} subPrice={pricePeriod} />
|
|
||||||
|
{plansPeriod === SubscriptionPlansPeriod.Monthly ? (
|
||||||
|
<PricingPlan.Price price={monthlyPrice} subPrice={monthlyPriceLabel} />
|
||||||
|
) : (
|
||||||
|
<PricingPlan.Price
|
||||||
|
price={annuallyPrice}
|
||||||
|
subPrice={annuallyPriceLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<PricingPlan.BuyButton loading={isLoading} onClick={handleClick}>
|
<PricingPlan.BuyButton loading={isLoading} onClick={handleClick}>
|
||||||
Subscribe
|
Subscribe
|
||||||
</PricingPlan.BuyButton>
|
</PricingPlan.BuyButton>
|
||||||
|
|
||||||
<PricingPlan.Features>
|
<PricingPlan.Features>
|
||||||
{features?.map((feature) => (
|
{features?.map((feature) => (
|
||||||
<PricingPlan.FeatureLine>{feature}</PricingPlan.FeatureLine>
|
<PricingPlan.FeatureLine
|
||||||
|
hintLabel={feature.hintLabel}
|
||||||
|
hintContent={feature.hint}
|
||||||
|
>
|
||||||
|
{feature.text}
|
||||||
|
</PricingPlan.FeatureLine>
|
||||||
))}
|
))}
|
||||||
</PricingPlan.Features>
|
</PricingPlan.Features>
|
||||||
</PricingPlan>
|
</PricingPlan>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubscriptionPlans({ plans }) {
|
export const SubscriptionPlan = R.compose(
|
||||||
return (
|
withPlans(({ plansPeriod }) => ({ plansPeriod })),
|
||||||
<Group spacing={18} noWrap align='stretch'>
|
)(SubscriptionPlanRoot);
|
||||||
{plans.map((plan, index) => (
|
|
||||||
<SubscriptionPricing
|
|
||||||
key={index}
|
|
||||||
slug={plan.slug}
|
|
||||||
label={plan.name}
|
|
||||||
description={plan.description}
|
|
||||||
features={plan.features}
|
|
||||||
featured={plan.featured}
|
|
||||||
price={plan.price}
|
|
||||||
pricePeriod={plan.pricePeriod}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Group } from '@/components';
|
||||||
|
import { SubscriptionPlan } from './SubscriptionPlan';
|
||||||
|
import { useSubscriptionPlans } from './hooks';
|
||||||
|
|
||||||
|
export function SubscriptionPlans() {
|
||||||
|
const subscriptionPlans = useSubscriptionPlans();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group spacing={14} noWrap align="stretch">
|
||||||
|
{subscriptionPlans.map((plan, index) => (
|
||||||
|
<SubscriptionPlan
|
||||||
|
key={index}
|
||||||
|
slug={plan.slug}
|
||||||
|
label={plan.name}
|
||||||
|
description={plan.description}
|
||||||
|
features={plan.features}
|
||||||
|
featured={plan.featured}
|
||||||
|
monthlyPrice={plan.monthlyPrice}
|
||||||
|
monthlyPriceLabel={plan.monthlyPriceLabel}
|
||||||
|
annuallyPrice={plan.annuallyPrice}
|
||||||
|
annuallyPriceLabel={plan.annuallyPriceLabel}
|
||||||
|
monthlyVariantId={plan.monthlyVariantId}
|
||||||
|
annuallyVariantId={plan.annuallyVariantId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Intent, Switch, Tag, Text } from '@blueprintjs/core';
|
||||||
|
import { Group } from '@/components';
|
||||||
|
import withSubscriptionPlansActions, {
|
||||||
|
WithSubscriptionPlansActionsProps,
|
||||||
|
} from '@/containers/Subscriptions/withSubscriptionPlansActions';
|
||||||
|
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
|
||||||
|
import styles from './SetupSubscription.module.scss';
|
||||||
|
|
||||||
|
interface SubscriptionPlansPeriodsSwitchCombinedProps
|
||||||
|
extends WithSubscriptionPlansActionsProps {}
|
||||||
|
|
||||||
|
function SubscriptionPlansPeriodSwitcherRoot({
|
||||||
|
// #withSubscriptionPlansActions
|
||||||
|
changeSubscriptionPlansPeriod,
|
||||||
|
}: SubscriptionPlansPeriodsSwitchCombinedProps) {
|
||||||
|
// Handles the period switch change.
|
||||||
|
const handleSwitchChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
changeSubscriptionPlansPeriod(
|
||||||
|
event.currentTarget.checked
|
||||||
|
? SubscriptionPlansPeriod.Annually
|
||||||
|
: SubscriptionPlansPeriod.Monthly,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}>
|
||||||
|
<Text>Pay Monthly</Text>
|
||||||
|
<Switch
|
||||||
|
large
|
||||||
|
onChange={handleSwitchChange}
|
||||||
|
className={styles.periodSwitch}
|
||||||
|
/>
|
||||||
|
<Text>
|
||||||
|
Pay Yearly{' '}
|
||||||
|
<Tag minimal intent={Intent.NONE}>
|
||||||
|
25% Off All Year
|
||||||
|
</Tag>
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubscriptionPlansPeriodSwitcher = R.compose(
|
||||||
|
withSubscriptionPlansActions,
|
||||||
|
)(SubscriptionPlansPeriodSwitcherRoot);
|
||||||
@@ -1,29 +1,21 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { Callout } from '@blueprintjs/core';
|
import { Callout } from '@blueprintjs/core';
|
||||||
import { SubscriptionPlans } from './SubscriptionPlan';
|
import { SubscriptionPlans } from './SubscriptionPlans';
|
||||||
import withPlans from '../../Subscriptions/withPlans';
|
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Billing plans.
|
* Billing plans.
|
||||||
*/
|
*/
|
||||||
function SubscriptionPlansSectionRoot({ plans }) {
|
export function SubscriptionPlansSection() {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<Callout
|
<Callout style={{ marginBottom: '2rem' }} icon={null}>
|
||||||
style={{ marginBottom: '1.5rem' }}
|
Simple plans. Simple prices. Only pay for what you really need. All
|
||||||
icon={null}
|
plans come with award-winning 24/7 customer support. Prices do not
|
||||||
title={'Early Adopter Plan'}
|
include applicable taxes.
|
||||||
>
|
|
||||||
We're looking for 200 early adopters, when you subscribe you'll get the
|
|
||||||
full features and unlimited users for a year regardless of the
|
|
||||||
subscribed plan.
|
|
||||||
</Callout>
|
</Callout>
|
||||||
<SubscriptionPlans plans={plans} />
|
|
||||||
|
<SubscriptionPlansPeriodSwitcher />
|
||||||
|
<SubscriptionPlans />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubscriptionPlansSection = compose(
|
|
||||||
withPlans(({ plans }) => ({ plans })),
|
|
||||||
)(SubscriptionPlansSectionRoot);
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { SubscriptionPlans } from '@/constants/subscriptionModels';
|
||||||
|
|
||||||
|
export const useSubscriptionPlans = () => {
|
||||||
|
return SubscriptionPlans;
|
||||||
|
};
|
||||||
@@ -1,17 +1,35 @@
|
|||||||
// @ts-nocheck
|
import { MapStateToProps, connect } from 'react-redux';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import {
|
import {
|
||||||
|
getPlansPeriodSelector,
|
||||||
getPlansSelector,
|
getPlansSelector,
|
||||||
} from '@/store/plans/plans.selectors';
|
} from '@/store/plans/plans.selectors';
|
||||||
|
import { ApplicationState } from '@/store/reducers';
|
||||||
|
|
||||||
export default (mapState) => {
|
export interface WithPlansProps {
|
||||||
const mapStateToProps = (state, props) => {
|
plans: ReturnType<ReturnType<typeof getPlansSelector>>;
|
||||||
|
plansPeriod: ReturnType<ReturnType<typeof getPlansPeriodSelector>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapState<Props> = (
|
||||||
|
mapped: WithPlansProps,
|
||||||
|
state: ApplicationState,
|
||||||
|
props: Props,
|
||||||
|
) => any;
|
||||||
|
|
||||||
|
export function withPlans<Props>(mapState?: MapState<Props>) {
|
||||||
|
const mapStateToProps: MapStateToProps<
|
||||||
|
WithPlansProps,
|
||||||
|
Props,
|
||||||
|
ApplicationState
|
||||||
|
> = (state, props) => {
|
||||||
const getPlans = getPlansSelector();
|
const getPlans = getPlansSelector();
|
||||||
|
const getPlansPeriod = getPlansPeriodSelector();
|
||||||
|
|
||||||
const mapped = {
|
const mapped = {
|
||||||
plans: getPlans(state, props),
|
plans: getPlans(state),
|
||||||
|
plansPeriod: getPlansPeriod(state),
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
return connect(mapStateToProps);
|
return connect(mapStateToProps);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
// @ts-nocheck
|
import { MapDispatchToProps, connect } from 'react-redux';
|
||||||
import { connect } from 'react-redux';
|
import {
|
||||||
import { initSubscriptionPlans } from '@/store/plans/plans.actions';
|
SubscriptionPlansPeriod,
|
||||||
|
changePlansPeriod,
|
||||||
|
initSubscriptionPlans,
|
||||||
|
} from '@/store/plans/plans.reducer';
|
||||||
|
|
||||||
export const mapDispatchToProps = (dispatch) => ({
|
export interface WithSubscriptionPlansActionsProps {
|
||||||
|
initSubscriptionPlans: () => void;
|
||||||
|
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapDispatchToProps: MapDispatchToProps<
|
||||||
|
WithSubscriptionPlansActionsProps,
|
||||||
|
{}
|
||||||
|
> = (dispatch: any) => ({
|
||||||
initSubscriptionPlans: () => dispatch(initSubscriptionPlans()),
|
initSubscriptionPlans: () => dispatch(initSubscriptionPlans()),
|
||||||
|
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) =>
|
||||||
|
dispatch(changePlansPeriod({ period })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps);
|
export default connect(null, mapDispatchToProps);
|
||||||
@@ -1,70 +1,46 @@
|
|||||||
// @ts-nocheck
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { createReducer } from '@reduxjs/toolkit';
|
import { SubscriptionPlans } from '@/constants/subscriptionModels';
|
||||||
import t from '@/store/types';
|
|
||||||
|
|
||||||
const getSubscriptionPlans = () => [
|
export enum SubscriptionPlansPeriod {
|
||||||
{
|
Monthly = 'monthly',
|
||||||
name: 'Capital Basic',
|
Annually = 'Annually',
|
||||||
slug: 'capital_basic',
|
}
|
||||||
description: 'Good for service businesses that just started.',
|
|
||||||
features: [
|
|
||||||
'Sale Invoices and Estimates',
|
|
||||||
'Tracking Expenses',
|
|
||||||
'Customize Invoice',
|
|
||||||
'Manual Journals',
|
|
||||||
'Bank Reconciliation',
|
|
||||||
'Chart of Accounts',
|
|
||||||
'Taxes',
|
|
||||||
'Basic Financial Reports & Insights',
|
|
||||||
],
|
|
||||||
price: '$29',
|
|
||||||
pricePeriod: 'Per Year',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Capital Plus',
|
|
||||||
slug: 'capital_plus',
|
|
||||||
description:
|
|
||||||
'Good for businesses have inventory and want more financial reports.',
|
|
||||||
features: [
|
|
||||||
'All Capital Basic features',
|
|
||||||
'Manage Bills',
|
|
||||||
'Inventory Tracking',
|
|
||||||
'Multi Currencies',
|
|
||||||
'Predefined user roles.',
|
|
||||||
'Transactions locking.',
|
|
||||||
'Smart Financial Reports.',
|
|
||||||
],
|
|
||||||
price: '$29',
|
|
||||||
pricePeriod: 'Per Year',
|
|
||||||
featured: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Capital Big',
|
|
||||||
slug: 'essentials',
|
|
||||||
description: 'Good for businesses have multiple inventory or branches.',
|
|
||||||
features: [
|
|
||||||
'All Capital Plus features',
|
|
||||||
'Multiple Warehouses',
|
|
||||||
'Multiple Branches',
|
|
||||||
'Invite >= 15 Users',
|
|
||||||
],
|
|
||||||
price: '$29',
|
|
||||||
pricePeriod: 'Per Year',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialState = {
|
interface StorePlansState {
|
||||||
plans: [],
|
plans: any;
|
||||||
periods: [],
|
plansPeriod: SubscriptionPlansPeriod;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default createReducer(initialState, {
|
export const SubscriptionPlansSlice = createSlice({
|
||||||
/**
|
name: 'plans',
|
||||||
* Initialize the subscription plans.
|
initialState: {
|
||||||
*/
|
plans: [],
|
||||||
[t.INIT_SUBSCRIPTION_PLANS]: (state) => {
|
periods: [],
|
||||||
const plans = getSubscriptionPlans();
|
plansPeriod: 'monthly',
|
||||||
|
} as StorePlansState,
|
||||||
|
reducers: {
|
||||||
|
/**
|
||||||
|
* Initialize the subscription plans.
|
||||||
|
* @param {StorePlansState} state
|
||||||
|
*/
|
||||||
|
initSubscriptionPlans: (state: StorePlansState) => {
|
||||||
|
const plans = SubscriptionPlans;
|
||||||
|
state.plans = plans;
|
||||||
|
},
|
||||||
|
|
||||||
state.plans = plans;
|
/**
|
||||||
|
* Changes the plans period (monthly or annually).
|
||||||
|
* @param {StorePlansState} state
|
||||||
|
* @param {PayloadAction<{ period: SubscriptionPlansPeriod }>} action
|
||||||
|
*/
|
||||||
|
changePlansPeriod: (
|
||||||
|
state: StorePlansState,
|
||||||
|
action: PayloadAction<{ period: SubscriptionPlansPeriod }>,
|
||||||
|
) => {
|
||||||
|
state.plansPeriod = action.payload.period;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const { initSubscriptionPlans, changePlansPeriod } =
|
||||||
|
SubscriptionPlansSlice.actions;
|
||||||
|
|||||||
@@ -2,19 +2,21 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
const plansSelector = (state) => state.plans.plans;
|
const plansSelector = (state) => state.plans.plans;
|
||||||
const planSelector = (state, props) => state.plans.plans
|
const planSelector = (state, props) =>
|
||||||
.find((plan) => plan.slug === props.planSlug);
|
state.plans.plans.find((plan) => plan.slug === props.planSlug);
|
||||||
|
|
||||||
|
const plansPeriodSelector = (state) => state.plans.plansPeriod;
|
||||||
|
|
||||||
// Retrieve manual jounral current page results.
|
// Retrieve manual jounral current page results.
|
||||||
export const getPlansSelector = () => createSelector(
|
export const getPlansSelector = () =>
|
||||||
plansSelector,
|
createSelector(plansSelector, (plans) => {
|
||||||
(plans) => {
|
|
||||||
return plans;
|
return plans;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Retrieve plan details.
|
// Retrieve plan details.
|
||||||
export const getPlanSelector = () => createSelector(
|
export const getPlanSelector = () =>
|
||||||
planSelector,
|
createSelector(planSelector, (plan) => plan);
|
||||||
(plan) => plan,
|
|
||||||
)
|
// Retrieves the plans period (monthly or annually).
|
||||||
|
export const getPlansPeriodSelector = () =>
|
||||||
|
createSelector(plansPeriodSelector, (periods) => periods);
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ import paymentMades from './PaymentMades/paymentMades.reducer';
|
|||||||
import organizations from './organizations/organizations.reducers';
|
import organizations from './organizations/organizations.reducers';
|
||||||
import subscriptions from './subscription/subscription.reducer';
|
import subscriptions from './subscription/subscription.reducer';
|
||||||
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
|
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
|
||||||
import plans from './plans/plans.reducer';
|
import { SubscriptionPlansSlice } from './plans/plans.reducer';
|
||||||
import creditNotes from './CreditNote/creditNote.reducer';
|
import creditNotes from './CreditNote/creditNote.reducer';
|
||||||
import vendorCredit from './VendorCredit/VendorCredit.reducer';
|
import vendorCredit from './VendorCredit/VendorCredit.reducer';
|
||||||
import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer';
|
import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer';
|
||||||
import projects from './Project/projects.reducer';
|
import projects from './Project/projects.reducer';
|
||||||
import { PlaidSlice } from './banking/banking.reducer';
|
import { PlaidSlice } from './banking/banking.reducer';
|
||||||
|
|
||||||
|
export interface ApplicationState {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const appReducer = combineReducers({
|
const appReducer = combineReducers({
|
||||||
authentication,
|
authentication,
|
||||||
organizations,
|
organizations,
|
||||||
@@ -69,7 +73,7 @@ const appReducer = combineReducers({
|
|||||||
paymentReceives,
|
paymentReceives,
|
||||||
paymentMades,
|
paymentMades,
|
||||||
inventoryAdjustments,
|
inventoryAdjustments,
|
||||||
plans,
|
plans: SubscriptionPlansSlice.reducer,
|
||||||
creditNotes,
|
creditNotes,
|
||||||
vendorCredit,
|
vendorCredit,
|
||||||
warehouseTransfers,
|
warehouseTransfers,
|
||||||
|
|||||||
Reference in New Issue
Block a user