mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
Compare commits
16 Commits
fix-cashfl
...
v0.18.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
107a6f793b | ||
|
|
67d155759e | ||
|
|
7e2e87256f | ||
|
|
df7790d7c1 | ||
|
|
72128a72c4 | ||
|
|
eb3f23554f | ||
|
|
69ddf43b3e | ||
|
|
249eadaeaa | ||
|
|
59168bc691 | ||
|
|
81b26c6f13 | ||
|
|
da435d85d9 | ||
|
|
d096e49d45 | ||
|
|
73acdb6240 | ||
|
|
38d4122d11 | ||
|
|
24a77c81b3 | ||
|
|
7f41b4280e |
@@ -5,7 +5,8 @@ exports.up = function (knex) {
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
.inTable('uncategorized_cashflow_transactions')
|
||||
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
|
||||
table
|
||||
.integer('bank_rule_id')
|
||||
.unsigned()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
||||
table.integer('recognized_transaction_id').unsigned();
|
||||
table
|
||||
.integer('recognized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('recognized_bank_transactions')
|
||||
.withKeyName('uncategorizedCashflowTransRecognizedTranIdForeign');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||
table.increments('id');
|
||||
table.integer('uncategorized_transaction_id').unsigned();
|
||||
table
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id').unsigned();
|
||||
table.decimal('amount');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
IFinancialSheetCommonMeta,
|
||||
INumberFormatQuery,
|
||||
@@ -257,7 +258,6 @@ export interface IUncategorizedCashflowTransaction {
|
||||
categorized: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface CreateUncategorizedTransactionDTO {
|
||||
date: Date | string;
|
||||
accountId: number;
|
||||
@@ -269,3 +269,16 @@ export interface CreateUncategorizedTransactionDTO {
|
||||
plaidTransactionId?: string | null;
|
||||
batch?: string;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatingEventPayload {
|
||||
tenantId: number;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: any;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -130,8 +130,9 @@ export interface ICommandCashflowDeletedPayload {
|
||||
|
||||
export interface ICashflowTransactionCategorizedPayload {
|
||||
tenantId: number;
|
||||
cashflowTransactionId: number;
|
||||
uncategorizedTransaction: any;
|
||||
cashflowTransaction: ICashflowTransaction;
|
||||
categorizeDTO: any;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizingPayload {
|
||||
|
||||
8
packages/server/src/interfaces/Import.ts
Normal file
8
packages/server/src/interfaces/Import.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
||||
|
||||
|
||||
export interface IImportFileCommitedEventPayload {
|
||||
tenantId: number;
|
||||
importId: number;
|
||||
meta: ImportFilePreviewPOJO;
|
||||
}
|
||||
@@ -112,6 +112,7 @@ import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscr
|
||||
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
||||
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
||||
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -262,6 +263,7 @@ export const susbcribers = () => {
|
||||
UnlinkBankRuleOnDeleteBankRule,
|
||||
DecrementUncategorizedTransactionOnMatching,
|
||||
DecrementUncategorizedTransactionOnExclude,
|
||||
DecrementUncategorizedTransactionOnCategorize,
|
||||
|
||||
// Validate matching
|
||||
ValidateMatchingOnCashflowDelete,
|
||||
|
||||
@@ -184,56 +184,4 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the count of uncategorized transactions for the associated account
|
||||
* based on the specified operation.
|
||||
* @param {QueryContext} queryContext - The query context for the transaction.
|
||||
* @param {boolean} increment - Indicates whether to increment or decrement the count.
|
||||
*/
|
||||
private async updateUncategorizedTransactionCount(
|
||||
queryContext: QueryContext,
|
||||
increment: boolean,
|
||||
amount: number = 1
|
||||
) {
|
||||
const operation = increment ? 'increment' : 'decrement';
|
||||
|
||||
await Account.query(queryContext.transaction)
|
||||
.findById(this.accountId)
|
||||
[operation]('uncategorized_transactions', amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after insert.
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterInsert(queryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
await this.updateUncategorizedTransactionCount(queryContext, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after update.
|
||||
* @param {ModelOptions} opt
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterUpdate(
|
||||
opt: ModelOptions,
|
||||
queryContext: QueryContext
|
||||
): Promise<any> {
|
||||
await super.$afterUpdate(opt, queryContext);
|
||||
|
||||
if (this.id && this.categorized) {
|
||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after delete.
|
||||
* @param {QueryContext} queryContext
|
||||
*/
|
||||
public async $afterDelete(queryContext: QueryContext) {
|
||||
await super.$afterDelete(queryContext);
|
||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ export class DecrementUncategorizedTransactionOnMatching {
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
//
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
@@ -60,7 +59,6 @@ export class DecrementUncategorizedTransactionOnMatching {
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
//
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { Knex } from 'knex';
|
||||
import { uniqid } from 'uniqid';
|
||||
import uniqid from 'uniqid';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@@ -148,7 +148,6 @@ export class PlaidSyncDb {
|
||||
*/
|
||||
public async syncAccountsTransactions(
|
||||
tenantId: number,
|
||||
batchNo: string,
|
||||
plaidAccountsTransactions: PlaidTransaction[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
@@ -161,7 +160,6 @@ export class PlaidSyncDb {
|
||||
return this.syncAccountTranactions(
|
||||
tenantId,
|
||||
plaidAccountId,
|
||||
batchNo,
|
||||
plaidTransactions,
|
||||
trx
|
||||
);
|
||||
|
||||
@@ -73,8 +73,6 @@ export class PlaidUpdateTransactions {
|
||||
added.concat(modified),
|
||||
trx
|
||||
);
|
||||
// Sync removed transactions.
|
||||
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
|
||||
// Sync transactions cursor.
|
||||
await this.plaidSync.syncTransactionsCursor(
|
||||
tenantId,
|
||||
|
||||
@@ -37,7 +37,6 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||
(
|
||||
cashflowAccountId: number,
|
||||
creditAccountId: number,
|
||||
plaidTranasction: PlaidTransaction
|
||||
): CreateUncategorizedTransactionDTO => {
|
||||
return {
|
||||
|
||||
@@ -18,11 +18,11 @@ export class RegonizeTransactionsJob {
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId } = job.attrs.data;
|
||||
const { tenantId, batch } = job.attrs.data;
|
||||
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||
|
||||
try {
|
||||
await regonizeTransactions.recognizeTransactions(tenantId);
|
||||
await regonizeTransactions.recognizeTransactions(tenantId, batch);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
IBankRuleEventDeletedPayload,
|
||||
IBankRuleEventEditedPayload,
|
||||
} from '../../Rules/types';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class TriggerRecognizedTransactions {
|
||||
@@ -27,6 +29,10 @@ export class TriggerRecognizedTransactions {
|
||||
events.bankRules.onDeleted,
|
||||
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.import.onImportCommitted,
|
||||
this.triggerRecognizeTransactionsOnImportCommitted.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,4 +79,20 @@ export class TriggerRecognizedTransactions {
|
||||
const payload = { tenantId };
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the recognize bank transactions once the imported file commit.
|
||||
* @param {IImportFileCommitedEventPayload} payload -
|
||||
*/
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
tenantId,
|
||||
importId,
|
||||
meta,
|
||||
}: IImportFileCommitedEventPayload) {
|
||||
const importFile = await Import.query().findOne({ importId });
|
||||
const batch = importFile.paramsParsed.batch;
|
||||
const payload = { tenantId, batch };
|
||||
|
||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,20 +84,23 @@ export class CategorizeCashflowTransaction {
|
||||
cashflowTransactionDTO
|
||||
);
|
||||
// Updates the uncategorized transaction as categorized.
|
||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||
uncategorizedTransactionId,
|
||||
{
|
||||
categorized: true,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
categorizeRefId: cashflowTransaction.id,
|
||||
}
|
||||
);
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||
uncategorizedTransactionId,
|
||||
{
|
||||
categorized: true,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
categorizeRefId: cashflowTransaction.id,
|
||||
}
|
||||
);
|
||||
// Triggers `onCashflowTransactionCategorized` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionCategorized,
|
||||
{
|
||||
tenantId,
|
||||
// cashflowTransaction,
|
||||
cashflowTransaction,
|
||||
uncategorizedTransaction,
|
||||
categorizeDTO,
|
||||
trx,
|
||||
} as ICashflowTransactionCategorizedPayload
|
||||
);
|
||||
|
||||
@@ -2,7 +2,13 @@ import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
CreateUncategorizedTransactionDTO,
|
||||
IUncategorizedTransactionCreatedEventPayload,
|
||||
IUncategorizedTransactionCreatingEventPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class CreateUncategorizedTransaction {
|
||||
@@ -12,6 +18,9 @@ export class CreateUncategorizedTransaction {
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Creates an uncategorized cashflow transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -19,7 +28,7 @@ export class CreateUncategorizedTransaction {
|
||||
*/
|
||||
public create(
|
||||
tenantId: number,
|
||||
createDTO: CreateUncategorizedTransactionDTO,
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
@@ -27,12 +36,30 @@ export class CreateUncategorizedTransaction {
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
const transaction = await UncategorizedCashflowTransaction.query(
|
||||
trx
|
||||
).insertAndFetch({
|
||||
...createDTO,
|
||||
});
|
||||
return transaction;
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionUncategorizedCreating,
|
||||
{
|
||||
tenantId,
|
||||
createUncategorizedTransactionDTO,
|
||||
trx,
|
||||
} as IUncategorizedTransactionCreatingEventPayload
|
||||
);
|
||||
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx).insertAndFetch({
|
||||
...createUncategorizedTransactionDTO,
|
||||
});
|
||||
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
{
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
createUncategorizedTransactionDTO,
|
||||
trx,
|
||||
} as IUncategorizedTransactionCreatedEventPayload
|
||||
);
|
||||
return uncategorizedTransaction;
|
||||
},
|
||||
trx
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import * as yup from 'yup';
|
||||
import uniqid from 'uniqid';
|
||||
import { Importable } from '../Import/Importable';
|
||||
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
@@ -15,6 +16,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Passing the sheet DTO to create uncategorized transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -43,6 +45,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
return {
|
||||
...createDTO,
|
||||
accountId: context.import.paramsParsed.accountId,
|
||||
batch: context.import.paramsParsed.batch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,6 +57,9 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
return BankTransactionsSampleData;
|
||||
}
|
||||
|
||||
// ------------------
|
||||
// # Params
|
||||
// ------------------
|
||||
/**
|
||||
* Params validation schema.
|
||||
* @returns {ValidationSchema[]}
|
||||
@@ -79,4 +85,17 @@ export class UncategorizedTransactionsImportable extends Importable {
|
||||
await Account.query().findById(params.accountId).throwIfNotFound({});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the import params before storing them.
|
||||
* @param {Record<string, any>} parmas
|
||||
*/
|
||||
public transformParams(parmas: Record<string, any>) {
|
||||
const batch = uniqid();
|
||||
|
||||
return {
|
||||
...parmas,
|
||||
batch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
ICashflowTransactionCategorizedPayload,
|
||||
ICashflowTransactionUncategorizedPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnCategorize {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionCategorized,
|
||||
this.decrementUnCategorizedTransactionsOnCategorized.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionUncategorized,
|
||||
this.incrementUnCategorizedTransactionsOnUncategorized.bind(this)
|
||||
);
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionUncategorizedCreated,
|
||||
this.incrementUncategoirzedTransactionsOnCreated.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the uncategoirzed transactions on the account once categorizing.
|
||||
* @param {ICashflowTransactionCategorizedPayload}
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnCategorized({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
}: ICashflowTransactionCategorizedPayload) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query()
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the uncategorized transaction on the given account on uncategorizing.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
public async incrementUnCategorizedTransactionsOnUncategorized({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
}: ICashflowTransactionUncategorizedPayload) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query()
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments uncategorized transactions count once creating a new transaction.
|
||||
* @param {ICommandCashflowCreatedPayload} payload -
|
||||
*/
|
||||
public async incrementUncategoirzedTransactionsOnCreated({
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
trx,
|
||||
}: any) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
if (!uncategorizedTransaction.accountId) return;
|
||||
|
||||
await Account.query(trx)
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
const firstMatchedTrans = first(matchedTrans);
|
||||
|
||||
return (
|
||||
(firstCategorizedTrans?.id ||
|
||||
firstCategorizedTrans?.id ||
|
||||
firstMatchedTrans?.uncategorizedTransactionId ||
|
||||
null
|
||||
);
|
||||
|
||||
@@ -15,14 +15,10 @@ import { ServiceError } from '@/exceptions';
|
||||
import { getUniqueImportableValue, trimObject } from './_utils';
|
||||
import { ImportableResources } from './ImportableResources';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileCommon {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFileValidator: ImportFileDataValidator;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcessCommit {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Commits the imported file.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async commit(
|
||||
tenantId: number,
|
||||
importId: number
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||
|
||||
const meta = await this.importFile.import(tenantId, importId, trx);
|
||||
|
||||
// Commit the successed transaction.
|
||||
await trx.commit();
|
||||
|
||||
// Triggers `onImportFileCommitted` event.
|
||||
await this.eventPublisher.emitAsync(events.import.onImportCommitted, {
|
||||
meta,
|
||||
importId,
|
||||
tenantId,
|
||||
} as IImportFileCommitedEventPayload);
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { ImportFilePreview } from './ImportFilePreview';
|
||||
import { ImportSampleService } from './ImportSample';
|
||||
import { ImportFileMeta } from './ImportFileMeta';
|
||||
import { ImportFileProcessCommit } from './ImportFileProcessCommit';
|
||||
|
||||
@Inject()
|
||||
export class ImportResourceApplication {
|
||||
@@ -27,6 +28,9 @@ export class ImportResourceApplication {
|
||||
@Inject()
|
||||
private importMetaService: ImportFileMeta;
|
||||
|
||||
@Inject()
|
||||
private importProcessCommit: ImportFileProcessCommit;
|
||||
|
||||
/**
|
||||
* Reads the imported file and stores the import file meta under unqiue id.
|
||||
* @param {number} tenantId -
|
||||
@@ -74,12 +78,12 @@ export class ImportResourceApplication {
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async process(tenantId: number, importId: number) {
|
||||
return this.importProcessService.import(tenantId, importId);
|
||||
return this.importProcessCommit.commit(tenantId, importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the import meta of the given import id.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} tenantId -
|
||||
* @param {string} importId - Import id.
|
||||
* @returns {}
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import config from '@/config';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
} from './utils';
|
||||
import { Plan } from '@/system/models';
|
||||
import { Subscription } from './Subscription';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export class LemonSqueezyWebhooks {
|
||||
@@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks {
|
||||
private subscriptionService: Subscription;
|
||||
|
||||
/**
|
||||
* handle the LemonSqueezy webhooks.
|
||||
* Handles the Lemon Squeezy webhooks.
|
||||
* @param {string} rawBody
|
||||
* @param {string} signature
|
||||
* @returns {Promise<void>}
|
||||
@@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks {
|
||||
const variantId = attributes.variant_id as string;
|
||||
|
||||
// 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) {
|
||||
throw new Error(`Plan with variantId ${variantId} not found.`);
|
||||
@@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks {
|
||||
// Update the subscription in the database.
|
||||
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.
|
||||
if (webhookEvent === 'subscription_created') {
|
||||
await this.subscriptionService.newSubscribtion(
|
||||
tenantId,
|
||||
'early-adaptor'
|
||||
);
|
||||
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
|
||||
}
|
||||
}
|
||||
} else if (webhookEvent.startsWith('order_')) {
|
||||
|
||||
@@ -399,6 +399,9 @@ export default {
|
||||
onTransactionCategorizing: 'onTransactionCategorizing',
|
||||
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
||||
|
||||
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
|
||||
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
|
||||
|
||||
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
||||
onTransactionUncategorized: 'onTransactionUncategorized',
|
||||
|
||||
@@ -647,4 +650,9 @@ export default {
|
||||
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||
},
|
||||
|
||||
// Import files.
|
||||
import: {
|
||||
onImportCommitted: 'onImportFileCommitted',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('subscription_plans', (table) => {
|
||||
table.string('lemon_variant_id').nullable().index();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => {
|
||||
return knex.schema.table('subscription_plans', (table) => {
|
||||
table.dropColumn('lemon_variant_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
exports.up = function (knex) {
|
||||
return knex('subscription_plans').insert([
|
||||
// Capital Basic
|
||||
{
|
||||
name: 'Capital Basic (Monthly)',
|
||||
slug: 'capital-basic-monthly',
|
||||
price: 10,
|
||||
active: true,
|
||||
currency: 'USD',
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'month',
|
||||
lemon_variant_id: '446152',
|
||||
// lemon_variant_id: '450016',
|
||||
},
|
||||
{
|
||||
name: 'Capital Basic (Annually)',
|
||||
slug: 'capital-basic-annually',
|
||||
price: 90,
|
||||
active: true,
|
||||
currency: 'USD',
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'year',
|
||||
lemon_variant_id: '446153',
|
||||
// lemon_variant_id: '450018',
|
||||
},
|
||||
|
||||
// # Capital Essential
|
||||
{
|
||||
name: 'Capital Essential (Monthly)',
|
||||
slug: 'capital-essential-monthly',
|
||||
price: 20,
|
||||
active: true,
|
||||
currency: 'USD',
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'month',
|
||||
lemon_variant_id: '446155',
|
||||
// lemon_variant_id: '450028',
|
||||
},
|
||||
{
|
||||
name: 'Capital Essential (Annually)',
|
||||
slug: 'capital-essential-annually',
|
||||
price: 180,
|
||||
active: true,
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'year',
|
||||
lemon_variant_id: '446156',
|
||||
// lemon_variant_id: '450029',
|
||||
},
|
||||
|
||||
// # Capital Plus
|
||||
{
|
||||
name: 'Capital Plus (Monthly)',
|
||||
slug: 'capital-plus-monthly',
|
||||
price: 25,
|
||||
active: true,
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'month',
|
||||
lemon_variant_id: '446165',
|
||||
// lemon_variant_id: '450031',
|
||||
},
|
||||
{
|
||||
name: 'Capital Plus (Annually)',
|
||||
slug: 'capital-plus-annually',
|
||||
price: 228,
|
||||
active: true,
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'year',
|
||||
lemon_variant_id: '446164',
|
||||
// lemon_variant_id: '450032',
|
||||
},
|
||||
|
||||
// # Capital Big
|
||||
{
|
||||
name: 'Capital Big (Monthly)',
|
||||
slug: 'capital-big-monthly',
|
||||
price: 40,
|
||||
active: true,
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'month',
|
||||
lemon_variant_id: '446167',
|
||||
// lemon_variant_id: '450024',
|
||||
},
|
||||
{
|
||||
name: 'Capital Big (Annually)',
|
||||
slug: 'capital-big-annually',
|
||||
price: 360,
|
||||
active: true,
|
||||
invoice_period: 1,
|
||||
invoice_interval: 'year',
|
||||
lemon_variant_id: '446168',
|
||||
// lemon_variant_id: '450025',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
exports.down = function (knex) {};
|
||||
@@ -23,9 +23,10 @@
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2F343C;
|
||||
|
||||
@@ -47,13 +48,31 @@
|
||||
}
|
||||
.price {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #404854;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #252A31;
|
||||
}
|
||||
|
||||
.pricePer{
|
||||
color: #738091;
|
||||
font-size: 12px;
|
||||
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 { Box, Group, Stack } from '../Layout';
|
||||
import styles from './PricingPlan.module.scss';
|
||||
@@ -64,7 +71,7 @@ export interface PricingPriceProps {
|
||||
*/
|
||||
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
||||
return (
|
||||
<Stack spacing={6} className={styles.priceRoot}>
|
||||
<Stack spacing={4} className={styles.priceRoot}>
|
||||
<h4 className={styles.price}>{price}</h4>
|
||||
<span className={styles.pricePer}>{subPrice}</span>
|
||||
</Stack>
|
||||
@@ -101,7 +108,7 @@ export interface PricingFeaturesProps {
|
||||
*/
|
||||
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
||||
return (
|
||||
<Stack spacing={10} className={styles.features}>
|
||||
<Stack spacing={14} className={styles.features}>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
@@ -109,15 +116,41 @@ PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
||||
|
||||
export interface PricingFeatureLineProps {
|
||||
children: React.ReactNode;
|
||||
hintContent?: string;
|
||||
hintLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a single feature line within a list of features.
|
||||
* @param children - The content of the feature line.
|
||||
*/
|
||||
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
|
||||
return (
|
||||
<Group noWrap spacing={12}>
|
||||
PricingPlan.FeatureLine = ({
|
||||
children,
|
||||
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} />
|
||||
<Box className={styles.featureItem}>{children}</Box>
|
||||
</Group>
|
||||
|
||||
@@ -39,3 +39,12 @@ export const TRANSACRIONS_TYPE = [
|
||||
'OtherExpense',
|
||||
'TransferToAccount',
|
||||
];
|
||||
|
||||
export const MoneyCategoryPerCreditAccountRootType = {
|
||||
OwnerContribution: ['equity'],
|
||||
OtherIncome: ['income'],
|
||||
OwnerDrawing: ['equity'],
|
||||
OtherExpense: ['expense'],
|
||||
TransferToAccount: ['asset'],
|
||||
TransferFromAccount: ['asset'],
|
||||
};
|
||||
|
||||
@@ -1,10 +1,140 @@
|
||||
// @ts-nocheck
|
||||
// Subscription plans.
|
||||
export const plans = [
|
||||
|
||||
];
|
||||
interface SubscriptionPlanFeature {
|
||||
text: string;
|
||||
hint?: string;
|
||||
label?: string;
|
||||
style?: Record<string, string>;
|
||||
}
|
||||
interface SubscriptionPlan {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
features: SubscriptionPlanFeature[];
|
||||
featured?: boolean;
|
||||
monthlyPrice: string;
|
||||
monthlyPriceLabel: string;
|
||||
annuallyPrice: string;
|
||||
annuallyPriceLabel: string;
|
||||
monthlyVariantId: string;
|
||||
annuallyVariantId: string;
|
||||
}
|
||||
|
||||
// Payment methods.
|
||||
export const paymentMethods = [
|
||||
|
||||
];
|
||||
export const SubscriptionPlans = [
|
||||
{
|
||||
name: 'Capital Basic',
|
||||
slug: 'capital_basic',
|
||||
description: 'Good for service businesses that just started.',
|
||||
features: [
|
||||
{
|
||||
text: 'Unlimited Sale Invoices',
|
||||
hintLabel: 'Unlimited Sale Invoices',
|
||||
hint: 'Good for service businesses that just started for service businesses that just started',
|
||||
},
|
||||
{ text: 'Unlimated Sale Estimates' },
|
||||
{ text: 'Track GST and VAT' },
|
||||
{ text: 'Connect Banks for Automatic Importing' },
|
||||
{ text: 'Chart of Accounts' },
|
||||
{
|
||||
text: 'Manual Journals',
|
||||
hintLabel: 'Manual Journals',
|
||||
hint: 'Write manual journals entries for financial transactions not automatically captured by the system to adjust financial statements.',
|
||||
},
|
||||
{
|
||||
text: 'Basic Financial Reports & Insights',
|
||||
hint: 'Balance sheet, profit & loss statement, cashflow statement, general ledger, journal sheet, A/P aging summary, A/R aging summary',
|
||||
},
|
||||
{ text: 'Unlimited User Seats' },
|
||||
],
|
||||
monthlyPrice: '$10',
|
||||
monthlyPriceLabel: 'Per month',
|
||||
annuallyPrice: '$7.5',
|
||||
annuallyPriceLabel: 'Per month',
|
||||
monthlyVariantId: '446152',
|
||||
// monthlyVariantId: '450016',
|
||||
annuallyVariantId: '446153',
|
||||
// annuallyVariantId: '450018',
|
||||
},
|
||||
{
|
||||
name: 'Capital Essential',
|
||||
slug: 'capital_plus',
|
||||
description: 'Good for have inventory and want more financial reports.',
|
||||
features: [
|
||||
{ text: 'All Capital Basic features' },
|
||||
{ text: 'Purchase Invoices' },
|
||||
{
|
||||
text: 'Multi Currency Transactions',
|
||||
hintLabel: 'Multi Currency',
|
||||
hint: 'Pay and get paid and do manual journals in any currency with real time exchange rates conversions.',
|
||||
},
|
||||
{
|
||||
text: 'Transactions Locking',
|
||||
hintLabel: 'Transactions Locking',
|
||||
hint: 'Transaction Locking freezes transactions to prevent any additions, modifications, or deletions of transactions recorded during the specified date.',
|
||||
},
|
||||
{
|
||||
text: 'Inventory Tracking',
|
||||
hintLabel: 'Inventory Tracking',
|
||||
hint: 'Track goods in the stock, cost of goods, and get notifications when quantity is low.',
|
||||
},
|
||||
{ text: 'Smart Financial Reports' },
|
||||
{ text: 'Advanced Inventory Reports' },
|
||||
],
|
||||
monthlyPrice: '$20',
|
||||
monthlyPriceLabel: 'Per month',
|
||||
annuallyPrice: '$15',
|
||||
annuallyPriceLabel: 'Per month',
|
||||
// monthlyVariantId: '450028',
|
||||
monthlyVariantId: '446155',
|
||||
// annuallyVariantId: '450029',
|
||||
annuallyVariantId: '446156',
|
||||
},
|
||||
{
|
||||
name: 'Capital Plus',
|
||||
slug: 'essentials',
|
||||
description: 'Good for business want financial and access control.',
|
||||
features: [
|
||||
{ text: 'All Capital Essential features' },
|
||||
{ text: 'Custom User Roles Access' },
|
||||
{ text: 'Vendor Credits' },
|
||||
{
|
||||
text: 'Budgeting',
|
||||
hint: 'Create multiple budgets and compare targets with actuals to understand how your business is performing.',
|
||||
},
|
||||
{ text: 'Analysis Cost Center' },
|
||||
],
|
||||
monthlyPrice: '$25',
|
||||
monthlyPriceLabel: 'Per month',
|
||||
annuallyPrice: '$19',
|
||||
annuallyPriceLabel: 'Per month',
|
||||
featured: true,
|
||||
// monthlyVariantId: '450031',
|
||||
monthlyVariantId: '446165',
|
||||
// annuallyVariantId: '450032',
|
||||
annuallyVariantId: '446164',
|
||||
},
|
||||
{
|
||||
name: 'Capital Big',
|
||||
slug: 'essentials',
|
||||
description: 'Good for businesses have multiple branches.',
|
||||
features: [
|
||||
{ text: 'All Capital Plus features' },
|
||||
{
|
||||
text: 'Multiple Branches',
|
||||
hintLabel: '',
|
||||
hint: 'Track the organization transactions and accounts in multiple branches.',
|
||||
},
|
||||
{
|
||||
text: 'Multiple Warehouses',
|
||||
hintLabel: 'Multiple Warehouses',
|
||||
hint: 'Track the organization inventory in multiple warehouses and transfer goods between them.',
|
||||
},
|
||||
],
|
||||
monthlyPrice: '$40',
|
||||
monthlyPriceLabel: 'Per month',
|
||||
annuallyPrice: '$30',
|
||||
annuallyPriceLabel: 'Per month',
|
||||
// monthlyVariantId: '450024',
|
||||
monthlyVariantId: '446167',
|
||||
// annuallyVariantId: '450025',
|
||||
annuallyVariantId: '446168',
|
||||
},
|
||||
] as SubscriptionPlan[];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
@@ -16,11 +17,11 @@ import {
|
||||
} from '@/components';
|
||||
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
|
||||
import {
|
||||
AssignTransactionTypeOptions,
|
||||
FieldCondition,
|
||||
Fields,
|
||||
RuleFormValues,
|
||||
TransactionTypeOptions,
|
||||
getAccountRootFromMoneyCategory,
|
||||
initialValues,
|
||||
} from './_utils';
|
||||
import { useRuleFormDialogBoot } from './RuleFormBoot';
|
||||
@@ -31,6 +32,11 @@ import {
|
||||
} from '@/utils';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
||||
|
||||
// Retrieves the add money in button options.
|
||||
const MoneyInOptions = getAddMoneyInOptions();
|
||||
const MoneyOutOptions = getAddMoneyOutOptions();
|
||||
|
||||
function RuleFormContentFormRoot({
|
||||
// #withDialogActions
|
||||
@@ -47,7 +53,6 @@ function RuleFormContentFormRoot({
|
||||
...initialValues,
|
||||
...transformToForm(transformToCamelCase(bankRule), initialValues),
|
||||
};
|
||||
|
||||
// Handles the form submitting.
|
||||
const handleSubmit = (
|
||||
values: RuleFormValues,
|
||||
@@ -92,8 +97,9 @@ function RuleFormContentFormRoot({
|
||||
label={'Rule Name'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={'name'} />
|
||||
<FInputGroup name={'name'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
@@ -101,29 +107,22 @@ function RuleFormContentFormRoot({
|
||||
label={'Apply the rule to account'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 350 }}
|
||||
fastField
|
||||
>
|
||||
<AccountsSelect
|
||||
name={'applyIfAccountId'}
|
||||
items={accounts}
|
||||
filterByTypes={['cash', 'bank']}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'applyIfTransactionType'}
|
||||
label={'Apply to transactions are'}
|
||||
style={{ maxWidth: 350 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'applyIfTransactionType'}
|
||||
items={TransactionTypeOptions}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
<RuleApplyIfTransactionTypeField />
|
||||
|
||||
<FFormGroup
|
||||
name={'conditionsType'}
|
||||
label={'Categorize the transactions when'}
|
||||
fastField
|
||||
>
|
||||
<FRadioGroup name={'conditionsType'}>
|
||||
<Radio value={'and'} label={'All the following criteria matches'} />
|
||||
@@ -139,34 +138,16 @@ function RuleFormContentFormRoot({
|
||||
Then Assign
|
||||
</h3>
|
||||
|
||||
<FFormGroup
|
||||
name={'assignCategory'}
|
||||
label={'Transaction type'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'assignCategory'}
|
||||
items={AssignTransactionTypeOptions}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'assignAccountId'}
|
||||
label={'Account category'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
>
|
||||
<AccountsSelect name={'assignAccountId'} items={accounts} />
|
||||
</FFormGroup>
|
||||
<RuleAssignCategoryField />
|
||||
<RuleAssignCategoryAccountField />
|
||||
|
||||
<FFormGroup
|
||||
name={'assignRef'}
|
||||
label={'Reference'}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={'assignRef'} />
|
||||
<FInputGroup name={'assignRef'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<RuleFormActions />
|
||||
@@ -203,11 +184,13 @@ function RuleFormConditions() {
|
||||
name={`conditions[${index}].field`}
|
||||
label={'Field'}
|
||||
style={{ marginBottom: 0, flex: '1 0' }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={`conditions[${index}].field`}
|
||||
items={Fields}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
@@ -215,11 +198,13 @@ function RuleFormConditions() {
|
||||
name={`conditions[${index}].comparator`}
|
||||
label={'Condition'}
|
||||
style={{ marginBottom: 0, flex: '1 0' }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={`conditions[${index}].comparator`}
|
||||
items={FieldCondition}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
@@ -227,8 +212,9 @@ function RuleFormConditions() {
|
||||
name={`conditions[${index}].value`}
|
||||
label={'Value'}
|
||||
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={`conditions[${index}].value`} />
|
||||
<FInputGroup name={`conditions[${index}].value`} fastField />
|
||||
</FFormGroup>
|
||||
</Group>
|
||||
))}
|
||||
@@ -284,3 +270,104 @@ function RuleFormActionsRoot({
|
||||
}
|
||||
|
||||
const RuleFormActions = R.compose(withDialogActions)(RuleFormActionsRoot);
|
||||
|
||||
function RuleApplyIfTransactionTypeField() {
|
||||
const { setFieldValue } = useFormikContext<RuleFormValues>();
|
||||
|
||||
const handleItemChange = useCallback(
|
||||
(item: any) => {
|
||||
setFieldValue('applyIfTransactionType', item.value);
|
||||
setFieldValue('assignCategory', '');
|
||||
setFieldValue('assignAccountId', '');
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'applyIfTransactionType'}
|
||||
label={'Apply to transactions are'}
|
||||
style={{ maxWidth: 350 }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={'applyIfTransactionType'}
|
||||
items={TransactionTypeOptions}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
onItemChange={handleItemChange}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleAssignCategoryField() {
|
||||
const { values, setFieldValue } = useFormikContext<RuleFormValues>();
|
||||
|
||||
// Retrieves the transaction types if it is deposit or withdrawal.
|
||||
const transactionTypes = useMemo(
|
||||
() =>
|
||||
values?.applyIfTransactionType === 'deposit'
|
||||
? MoneyInOptions
|
||||
: MoneyOutOptions,
|
||||
[values?.applyIfTransactionType],
|
||||
);
|
||||
|
||||
// Handles the select item change.
|
||||
const handleItemChange = useCallback(
|
||||
(item: any) => {
|
||||
setFieldValue('assignCategory', item.value);
|
||||
setFieldValue('assignAccountId', '');
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'assignCategory'}
|
||||
label={'Transaction type'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
>
|
||||
<FSelect
|
||||
name={'assignCategory'}
|
||||
items={transactionTypes}
|
||||
popoverProps={{ minimal: true, inline: false }}
|
||||
valueAccessor={'value'}
|
||||
textAccessor={'name'}
|
||||
onItemChange={handleItemChange}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleAssignCategoryAccountField() {
|
||||
const { values } = useFormikContext<RuleFormValues>();
|
||||
const { accounts } = useRuleFormDialogBoot();
|
||||
|
||||
const accountRoot = useMemo(
|
||||
() => getAccountRootFromMoneyCategory(values.assignCategory),
|
||||
[values.assignCategory],
|
||||
);
|
||||
|
||||
return (
|
||||
<FFormGroup
|
||||
name={'assignAccountId'}
|
||||
label={'Account category'}
|
||||
labelInfo={<Tag minimal>Required</Tag>}
|
||||
style={{ maxWidth: 300 }}
|
||||
fastField
|
||||
shouldUpdateDeps={{ accountRoot }}
|
||||
>
|
||||
<AccountsSelect
|
||||
name={'assignAccountId'}
|
||||
items={accounts}
|
||||
filterByRootTypes={accountRoot}
|
||||
shouldUpdateDeps={{ accountRoot }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { camelCase, get, upperFirst } from 'lodash';
|
||||
import { MoneyCategoryPerCreditAccountRootType } from '@/constants/cashflowOptions';
|
||||
|
||||
export const initialValues = {
|
||||
name: '',
|
||||
order: 0,
|
||||
applyIfAccountId: '',
|
||||
applyIfTransactionType: '',
|
||||
applyIfTransactionType: 'deposit',
|
||||
conditionsType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
@@ -47,3 +50,9 @@ export const FieldCondition = [
|
||||
export const AssignTransactionTypeOptions = [
|
||||
{ value: 'expense', text: 'Expense' },
|
||||
];
|
||||
|
||||
export const getAccountRootFromMoneyCategory = (category: string): string[] => {
|
||||
const _category = upperFirst(camelCase(category));
|
||||
|
||||
return get(MoneyCategoryPerCreditAccountRootType, _category) || [];
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Tooltip,
|
||||
MenuDivider,
|
||||
} from '@blueprintjs/core';
|
||||
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
||||
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||
@@ -213,9 +212,8 @@ export function useAccountUncategorizedTransactionsColumns() {
|
||||
{
|
||||
id: 'reference_number',
|
||||
Header: 'Ref.#',
|
||||
accessor: 'reference_number',
|
||||
accessor: 'reference_no',
|
||||
width: 50,
|
||||
className: 'reference_number',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherIncome() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerContribution() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionTransferFrom() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherExpense() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerDrawings() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FTextArea,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionToAccount() {
|
||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export const MatchingReconcileFormSchema = Yup.object().shape({
|
||||
type: Yup.string().required().label('Type'),
|
||||
date: Yup.string().required().label('Date'),
|
||||
amount: Yup.string().required().label('Amount'),
|
||||
memo: Yup.string().required().label('Memo'),
|
||||
memo: Yup.string().required().min(3).label('Memo'),
|
||||
referenceNo: Yup.string().label('Refernece #'),
|
||||
category: Yup.string().required().label('Categogry'),
|
||||
});
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Button, Intent, Position, Tag } from '@blueprintjs/core';
|
||||
import {
|
||||
Form,
|
||||
Formik,
|
||||
FormikHelpers,
|
||||
FormikValues,
|
||||
useFormikContext,
|
||||
} from 'formik';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { round } from 'lodash';
|
||||
import {
|
||||
AccountsSelect,
|
||||
AppToaster,
|
||||
@@ -19,6 +14,7 @@ import {
|
||||
FInputGroup,
|
||||
FMoneyInputGroup,
|
||||
Group,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import { Aside } from '@/components/Aside/Aside';
|
||||
import { momentFormatter } from '@/utils';
|
||||
@@ -100,7 +96,7 @@ function MatchingReconcileTransactionFormRoot({
|
||||
|
||||
const _initialValues = {
|
||||
...initialValues,
|
||||
amount: Math.abs(reconcileMatchingTransactionPendingAmount) || 0,
|
||||
amount: round(Math.abs(reconcileMatchingTransactionPendingAmount), 2) || 0,
|
||||
date: moment().format('YYYY-MM-DD'),
|
||||
type:
|
||||
reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal',
|
||||
@@ -179,7 +175,7 @@ function CreateReconcileTransactionContent() {
|
||||
},
|
||||
boundary: 'viewport',
|
||||
}}
|
||||
inputProps={{ fill: true }}
|
||||
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||
fill
|
||||
fastField
|
||||
/>
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.periodSwitch {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,32 +1,65 @@
|
||||
// @ts-nocheck
|
||||
import { AppToaster, Group, T } from '@/components';
|
||||
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
|
||||
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 { 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 {
|
||||
slug: string;
|
||||
label: string;
|
||||
description: string;
|
||||
features?: Array<String>;
|
||||
features?: Array<SubscriptionPricingFeature>;
|
||||
featured?: boolean;
|
||||
price: string;
|
||||
pricePeriod: string;
|
||||
monthlyPrice: string;
|
||||
monthlyPriceLabel: string;
|
||||
annuallyPrice: string;
|
||||
annuallyPriceLabel: string;
|
||||
monthlyVariantId?: string;
|
||||
annuallyVariantId?: string;
|
||||
}
|
||||
|
||||
function SubscriptionPricing({
|
||||
featured,
|
||||
interface SubscriptionPricingCombinedProps
|
||||
extends SubscriptionPricingProps,
|
||||
WithPlansProps {}
|
||||
|
||||
function SubscriptionPlanRoot({
|
||||
label,
|
||||
description,
|
||||
featured,
|
||||
features,
|
||||
price,
|
||||
pricePeriod,
|
||||
}: SubscriptionPricingProps) {
|
||||
monthlyPrice,
|
||||
monthlyPriceLabel,
|
||||
annuallyPrice,
|
||||
annuallyPriceLabel,
|
||||
monthlyVariantId,
|
||||
annuallyVariantId,
|
||||
|
||||
// #withPlans
|
||||
plansPeriod,
|
||||
}: SubscriptionPricingCombinedProps) {
|
||||
const { mutateAsync: getLemonCheckout, isLoading } =
|
||||
useGetLemonSqueezyCheckout();
|
||||
|
||||
const handleClick = () => {
|
||||
getLemonCheckout({ variantId: '338516' })
|
||||
const variantId =
|
||||
SubscriptionPlansPeriod.Monthly === plansPeriod
|
||||
? monthlyVariantId
|
||||
: annuallyVariantId;
|
||||
|
||||
getLemonCheckout({ variantId })
|
||||
.then((res) => {
|
||||
const checkoutUrl = res.data.data.attributes.url;
|
||||
window.LemonSqueezy.Url.Open(checkoutUrl);
|
||||
@@ -42,37 +75,34 @@ function SubscriptionPricing({
|
||||
return (
|
||||
<PricingPlan featured={featured}>
|
||||
{featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>}
|
||||
|
||||
<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}>
|
||||
Subscribe
|
||||
</PricingPlan.BuyButton>
|
||||
|
||||
<PricingPlan.Features>
|
||||
{features?.map((feature) => (
|
||||
<PricingPlan.FeatureLine>{feature}</PricingPlan.FeatureLine>
|
||||
<PricingPlan.FeatureLine
|
||||
hintLabel={feature.hintLabel}
|
||||
hintContent={feature.hint}
|
||||
>
|
||||
{feature.text}
|
||||
</PricingPlan.FeatureLine>
|
||||
))}
|
||||
</PricingPlan.Features>
|
||||
</PricingPlan>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubscriptionPlans({ plans }) {
|
||||
return (
|
||||
<Group spacing={18} noWrap align='stretch'>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
export const SubscriptionPlan = R.compose(
|
||||
withPlans(({ plansPeriod }) => ({ plansPeriod })),
|
||||
)(SubscriptionPlanRoot);
|
||||
|
||||
@@ -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 { SubscriptionPlans } from './SubscriptionPlan';
|
||||
import withPlans from '../../Subscriptions/withPlans';
|
||||
import { compose } from '@/utils';
|
||||
import { SubscriptionPlans } from './SubscriptionPlans';
|
||||
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
|
||||
|
||||
/**
|
||||
* Billing plans.
|
||||
*/
|
||||
function SubscriptionPlansSectionRoot({ plans }) {
|
||||
export function SubscriptionPlansSection() {
|
||||
return (
|
||||
<section>
|
||||
<Callout
|
||||
style={{ marginBottom: '1.5rem' }}
|
||||
icon={null}
|
||||
title={'Early Adopter Plan'}
|
||||
>
|
||||
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 style={{ marginBottom: '2rem' }} icon={null}>
|
||||
Simple plans. Simple prices. Only pay for what you really need. All
|
||||
plans come with award-winning 24/7 customer support. Prices do not
|
||||
include applicable taxes.
|
||||
</Callout>
|
||||
<SubscriptionPlans plans={plans} />
|
||||
|
||||
<SubscriptionPlansPeriodSwitcher />
|
||||
<SubscriptionPlans />
|
||||
</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 { connect } from 'react-redux';
|
||||
import { MapStateToProps, connect } from 'react-redux';
|
||||
import {
|
||||
getPlansPeriodSelector,
|
||||
getPlansSelector,
|
||||
} from '@/store/plans/plans.selectors';
|
||||
import { ApplicationState } from '@/store/reducers';
|
||||
|
||||
export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
export interface WithPlansProps {
|
||||
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 getPlansPeriod = getPlansPeriodSelector();
|
||||
|
||||
const mapped = {
|
||||
plans: getPlans(state, props),
|
||||
plans: getPlans(state),
|
||||
plansPeriod: getPlansPeriod(state),
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import { connect } from 'react-redux';
|
||||
import { initSubscriptionPlans } from '@/store/plans/plans.actions';
|
||||
import { MapDispatchToProps, connect } from 'react-redux';
|
||||
import {
|
||||
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()),
|
||||
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) =>
|
||||
dispatch(changePlansPeriod({ period })),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
export default connect(null, mapDispatchToProps);
|
||||
|
||||
@@ -235,6 +235,12 @@ export function useExcludeUncategorizedTransaction(
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -282,6 +288,12 @@ export function useUnexcludeUncategorizedTransaction(
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...options,
|
||||
},
|
||||
@@ -323,6 +335,13 @@ export function useMatchUncategorizedTransaction(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
|
||||
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
@@ -362,6 +381,13 @@ export function useUnmatchMatchedUncategorizedTransaction(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
|
||||
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
|
||||
@@ -253,6 +253,9 @@ export function useCategorizeTransaction(props) {
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||
},
|
||||
...props,
|
||||
},
|
||||
@@ -276,6 +279,9 @@ export function useUncategorizeTransaction(props) {
|
||||
queryClient.invalidateQueries(
|
||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||
);
|
||||
|
||||
// Invalidate bank account summary.
|
||||
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||
},
|
||||
...props,
|
||||
},
|
||||
|
||||
@@ -1,70 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import t from '@/store/types';
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { SubscriptionPlans } from '@/constants/subscriptionModels';
|
||||
|
||||
const getSubscriptionPlans = () => [
|
||||
{
|
||||
name: 'Capital Basic',
|
||||
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',
|
||||
},
|
||||
];
|
||||
export enum SubscriptionPlansPeriod {
|
||||
Monthly = 'monthly',
|
||||
Annually = 'Annually',
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
plans: [],
|
||||
periods: [],
|
||||
};
|
||||
interface StorePlansState {
|
||||
plans: any;
|
||||
plansPeriod: SubscriptionPlansPeriod;
|
||||
}
|
||||
|
||||
export default createReducer(initialState, {
|
||||
/**
|
||||
* Initialize the subscription plans.
|
||||
*/
|
||||
[t.INIT_SUBSCRIPTION_PLANS]: (state) => {
|
||||
const plans = getSubscriptionPlans();
|
||||
export const SubscriptionPlansSlice = createSlice({
|
||||
name: 'plans',
|
||||
initialState: {
|
||||
plans: [],
|
||||
periods: [],
|
||||
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';
|
||||
|
||||
const plansSelector = (state) => state.plans.plans;
|
||||
const planSelector = (state, props) => state.plans.plans
|
||||
.find((plan) => plan.slug === props.planSlug);
|
||||
const planSelector = (state, props) =>
|
||||
state.plans.plans.find((plan) => plan.slug === props.planSlug);
|
||||
|
||||
const plansPeriodSelector = (state) => state.plans.plansPeriod;
|
||||
|
||||
// Retrieve manual jounral current page results.
|
||||
export const getPlansSelector = () => createSelector(
|
||||
plansSelector,
|
||||
(plans) => {
|
||||
export const getPlansSelector = () =>
|
||||
createSelector(plansSelector, (plans) => {
|
||||
return plans;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Retrieve plan details.
|
||||
export const getPlanSelector = () => createSelector(
|
||||
planSelector,
|
||||
(plan) => plan,
|
||||
)
|
||||
export const getPlanSelector = () =>
|
||||
createSelector(planSelector, (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 subscriptions from './subscription/subscription.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 vendorCredit from './VendorCredit/VendorCredit.reducer';
|
||||
import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer';
|
||||
import projects from './Project/projects.reducer';
|
||||
import { PlaidSlice } from './banking/banking.reducer';
|
||||
|
||||
export interface ApplicationState {
|
||||
|
||||
}
|
||||
|
||||
const appReducer = combineReducers({
|
||||
authentication,
|
||||
organizations,
|
||||
@@ -69,7 +73,7 @@ const appReducer = combineReducers({
|
||||
paymentReceives,
|
||||
paymentMades,
|
||||
inventoryAdjustments,
|
||||
plans,
|
||||
plans: SubscriptionPlansSlice.reducer,
|
||||
creditNotes,
|
||||
vendorCredit,
|
||||
warehouseTransfers,
|
||||
|
||||
Reference in New Issue
Block a user