Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
4b0780bf2a build(deps): bump mysql2 from 1.7.0 to 3.9.8
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 1.7.0 to 3.9.8.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/commits/v3.9.8)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-02 14:59:11 +00:00
48 changed files with 414 additions and 531 deletions

View File

@@ -75,9 +75,30 @@ PLAID_ENV=sandbox
# Your Plaid keys, which can be found in the Plaid Dashboard.
# https://dashboard.plaid.com/account/keys
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_SECRET_DEVELOPMENT=
PLAID_SECRET_SANDBOX=
PLAID_LINK_WEBHOOK=
# (Optional) Redirect URI settings section
# Only required for OAuth redirect URI testing (not common on desktop):
# Sandbox Mode:
# Set the PLAID_SANDBOX_REDIRECT_URI below to 'http://localhost:3001/oauth-link'.
# The OAuth redirect flow requires an endpoint on the developer's website
# that the bank website should redirect to. You will also need to configure
# this redirect URI for your client ID through the Plaid developer dashboard
# at https://dashboard.plaid.com/team/api.
# Development mode:
# When running in development mode, you must use an https:// url.
# You will need to configure this https:// redirect URI in the Plaid developer dashboard.
# Instructions to create a self-signed certificate for localhost can be found at
# https://github.com/plaid/pattern/blob/master/README.md#testing-oauth.
# If your system is not set up to run localhost with https://, you will be unable to test
# the OAuth in development and should leave the PLAID_DEVELOPMENT_REDIRECT_URI blank.
PLAID_SANDBOX_REDIRECT_URI=
PLAID_DEVELOPMENT_REDIRECT_URI=
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID=

View File

@@ -2,41 +2,6 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.17.0] - 04-06-2024
### New
* feat: Upload and attach documents by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/461
* feat: Export resource tables to pdf by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/460
* feat: Build and deploy develop Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/476
* feat: Internal docker virtual network by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/478
### Fixes
* fix: Skip send confirmation email if disabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/459
* fix: Lemon Squeezy redirect to base url by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/479
* fix: Organize Plaid env variables for development and sandbox envs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/480
* fix: Plaid syncs deposit imports as withdrawals by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/481
* fix: Validate the s3 configures exist by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/482
* fix: Run migrations only for initialized tenants by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/484
## [0.16.16] -
* feat: handle http exceptions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/456
* feat: add the missing Newrelic env vars to docker-compose.prod file by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/457
* fix: add the signup email confirmation env var by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/458
## [0.16.14] -
* fix: Typo in setup wizard by @ccantrell72 in https://github.com/bigcapitalhq/bigcapital/pull/440
* fix: Showing the real mail address on email confirmation view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/445
* fix: Auto-increment setting parsing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/453
## [0.16.12] -
* feat: Create a manifest list for `webapp` Docker image and push it to DockerHub. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/436
* feat: Combine arm64 and amd64 in one Github action runner by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/437
## [0.16.11] - 06-05-2024
### improvements

View File

@@ -22,15 +22,11 @@ services:
- server
- webapp
restart: on-failure
networks:
- bigcapital_network
webapp:
container_name: bigcapital-webapp
image: bigcapitalhq/webapp:latest
restart: on-failure
networks:
- bigcapital_network
server:
container_name: bigcapital-server
@@ -93,17 +89,14 @@ services:
- GOTENBERG_URL=${GOTENBERG_URL}
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
# Exchange Rate
- EXCHANGE_RATE_SERVICE=${EXCHANGE_RATE_SERVICE}
- OPEN_EXCHANGE_RATE_APP_ID-${OPEN_EXCHANGE_RATE_APP_ID}
# Bank Sync
- BANKING_CONNECT=${BANKING_CONNECT}
# Plaid
- PLAID_ENV=${PLAID_ENV}
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
- PLAID_SECRET=${PLAID_SECRET}
- PLAID_SECRET_DEVELOPMENT=${PLAID_SECRET_DEVELOPMENT}
- PLAID_SECRET_SANDBOX=${b8cf42b441e110451e2f69ad7e1e9f}
- PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK}
# Lemon Squeez
@@ -121,15 +114,6 @@ services:
- NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}
- NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME}
# S3
- S3_REGION=${S3_REGION}
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
- S3_ENDPOINT=${S3_ENDPOINT}
- S3_BUCKET=${S3_BUCKET}
networks:
- bigcapital_network
database_migration:
container_name: bigcapital-database-migration
build:
@@ -146,8 +130,6 @@ services:
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
depends_on:
- mysql
networks:
- bigcapital_network
mysql:
container_name: bigcapital-mysql
@@ -163,8 +145,6 @@ services:
- mysql:/var/lib/mysql
expose:
- '3306'
networks:
- bigcapital_network
mongo:
container_name: bigcapital-mongo
@@ -174,8 +154,6 @@ services:
- '27017'
volumes:
- mongo:/var/lib/mongodb
networks:
- bigcapital_network
redis:
container_name: bigcapital-redis
@@ -186,15 +164,11 @@ services:
- '6379'
volumes:
- redis:/data
networks:
- bigcapital_network
gotenberg:
image: gotenberg/gotenberg:7
expose:
- '9000'
networks:
- bigcapital_network
# Volumes
volumes:
@@ -209,8 +183,3 @@ volumes:
redis:
name: bigcapital_prod_redis
driver: local
# Networks
networks:
bigcapital_network:
driver: bridge

View File

@@ -86,7 +86,7 @@
"multer-s3": "^3.0.1",
"mustache": "^3.0.3",
"mysql": "^2.17.1",
"mysql2": "^1.6.5",
"mysql2": "^3.9.8",
"newrelic": "^11.15.0",
"node-cache": "^4.2.1",
"nodemailer": "^6.3.0",

View File

@@ -4,16 +4,12 @@ import { Router, Response, NextFunction, Request } from 'express';
import { body, param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';
import { AttachmentUploadPipeline } from '@/services/Attachments/S3UploadPipeline';
@Service()
export class AttachmentsController extends BaseController {
@Inject()
private attachmentsApplication: AttachmentsApplication;
@Inject()
private uploadPipelineService: AttachmentUploadPipeline;
/**
* Router constructor.
*/
@@ -22,8 +18,7 @@ export class AttachmentsController extends BaseController {
router.post(
'/',
this.uploadPipelineService.validateS3Configured,
this.uploadPipelineService.uploadPipeline().single('file'),
this.attachmentsApplication.uploadPipeline.single('file'),
this.validateUploadedFileExistance,
this.uploadAttachment.bind(this)
);

View File

@@ -1,6 +1,7 @@
import { NextFunction, Router, Request, Response } from 'express';
import { Inject, Service } from 'typedi';
import { Router } from 'express';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import { Request, Response } from 'express';
import { Inject, Service } from 'typedi';
import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
@@ -33,7 +34,7 @@ export class Webhooks extends BaseController {
* @param {Response} res
* @returns {Response}
*/
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
public async lemonWebhooks(req: Request, res: Response, next: any) {
const data = req.body;
const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody;
@@ -56,25 +57,20 @@ export class Webhooks extends BaseController {
* @param {Response} res
* @returns {Response}
*/
public async plaidWebhooks(req: Request, res: Response, next: NextFunction) {
public async plaidWebhooks(req: Request, res: Response) {
const { tenantId } = req;
const {
webhook_type: webhookType,
webhook_code: webhookCode,
item_id: plaidItemId,
} = req.body;
try {
const {
webhook_type: webhookType,
webhook_code: webhookCode,
item_id: plaidItemId,
} = req.body;
await this.plaidApp.webhooks(
tenantId,
plaidItemId,
webhookType,
webhookCode
);
return res.status(200).send({ code: 200, message: 'ok' });
} catch (error) {
next(error);
}
await this.plaidApp.webhooks(
tenantId,
plaidItemId,
webhookType,
webhookCode
);
return res.status(200).send({ code: 200, message: 'ok' });
}
}

View File

@@ -71,10 +71,6 @@ function getAllSystemTenants(knex) {
return knex('tenants');
}
function getAllInitializedSystemTenants(knex) {
return knex('tenants').whereNotNull('initializedAt');
}
// module.exports = {
// log,
// success,
@@ -187,7 +183,7 @@ commander
.action(async (cmd) => {
try {
const sysKnex = await initSystemKnex();
const tenants = await getAllInitializedSystemTenants(sysKnex);
const tenants = await getAllSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
@@ -224,6 +220,7 @@ commander
const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
}
Promise.all(migrateOpers).then(() => {
success('All tenants are migrated.');
});
@@ -283,3 +280,4 @@ commander
exit(error);
}
});

View File

@@ -204,7 +204,10 @@ module.exports = {
plaid: {
env: process.env.PLAID_ENV || 'sandbox',
clientId: process.env.PLAID_CLIENT_ID,
secret: process.env.PLAID_SECRET,
secretDevelopment: process.env.PLAID_SECRET_DEVELOPMENT,
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
},
@@ -215,7 +218,6 @@ module.exports = {
key: process.env.LEMONSQUEEZY_API_KEY,
storeId: process.env.LEMONSQUEEZY_STORE_ID,
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
redirectTo: `${process.env.BASE_URL}/setup`,
},
/**
@@ -235,6 +237,6 @@ module.exports = {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
bucket: process.env.S3_BUCKET,
},
};

View File

@@ -1,14 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('storage', (table) => {
table.increments('id').primary();
table.string('key').notNullable();
table.string('path').notNullable();
table.string('extension').notNullable();
table.integer('expire_in');
table.timestamps();
});
};
exports.down = function (knex) {
return knex.schema.dropTableIfExists('storage');
};

View File

@@ -1,5 +0,0 @@
exports.up = function (knex) {
return knex.schema.dropTableIfExists('storage');
};
exports.down = function (knex) {};

View File

@@ -164,7 +164,3 @@ export enum TaxRateAction {
DELETE = 'Delete',
VIEW = 'View',
}
export interface CreateAccountParams {
ignoreUniqueName: boolean;
}

View File

@@ -70,7 +70,10 @@ export class PlaidClientWrapper {
baseOptions: {
headers: {
'PLAID-CLIENT-ID': config.plaid.clientId,
'PLAID-SECRET': config.plaid.secret,
'PLAID-SECRET':
config.plaid.env === 'development'
? config.plaid.secretDevelopment
: config.plaid.secretSandbox,
'Plaid-Version': '2020-09-14',
},
},

View File

@@ -104,10 +104,10 @@ export default class UncategorizedCashflowTransaction extends mixin(
*/
private async updateUncategorizedTransactionCount(
queryContext: QueryContext,
increment: boolean,
amount: number = 1
increment: boolean
) {
const operation = increment ? 'increment' : 'decrement';
const amount = increment ? 1 : -1;
await Account.query(queryContext.transaction)
.findById(this.accountId)

View File

@@ -7,7 +7,6 @@ import {
IAccountEventCreatedPayload,
IAccountEventCreatingPayload,
IAccountCreateDTO,
CreateAccountParams,
} from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
@@ -31,22 +30,19 @@ export class CreateAccount {
/**
* Authorize the account creation.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
*/
private authorize = async (
tenantId: number,
accountDTO: IAccountCreateDTO,
baseCurrency: string,
params?: CreateAccountParams
baseCurrency: string
) => {
// Validate account name uniquiness.
if (!params.ignoreUniqueName) {
await this.validator.validateAccountNameUniquiness(
tenantId,
accountDTO.name
);
}
await this.validator.validateAccountNameUniquiness(
tenantId,
accountDTO.name
);
// Validate the account code uniquiness.
if (accountDTO.code) {
await this.validator.isAccountCodeUniqueOrThrowError(
@@ -86,7 +82,7 @@ export class CreateAccount {
/**
* Transformes the create account DTO to input model.
* @param {IAccountCreateDTO} createAccountDTO
* @param {IAccountCreateDTO} createAccountDTO
*/
private transformDTOToModel = (
createAccountDTO: IAccountCreateDTO,
@@ -108,8 +104,7 @@ export class CreateAccount {
public createAccount = async (
tenantId: number,
accountDTO: IAccountCreateDTO,
trx?: Knex.Transaction,
params: CreateAccountParams = { ignoreUniqueName: false }
trx?: Knex.Transaction
): Promise<IAccount> => {
const { Account } = this.tenancy.models(tenantId);
@@ -117,12 +112,8 @@ export class CreateAccount {
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Authorize the account creation.
await this.authorize(
tenantId,
accountDTO,
tenantMeta.baseCurrency,
params
);
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency);
// Transformes the DTO to model.
const accountInputModel = this.transformDTOToModel(
accountDTO,
@@ -157,4 +148,3 @@ export class CreateAccount {
);
};
}

View File

@@ -2,9 +2,11 @@ import { Inject, Service } from 'typedi';
import { UploadDocument } from './UploadDocument';
import { DeleteAttachment } from './DeleteAttachment';
import { GetAttachment } from './GetAttachment';
import { AttachmentUploadPipeline } from './S3UploadPipeline';
import { LinkAttachment } from './LinkAttachment';
import { UnlinkAttachment } from './UnlinkAttachment';
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
import type { Multer } from 'multer';
@Service()
export class AttachmentsApplication {
@@ -17,6 +19,9 @@ export class AttachmentsApplication {
@Inject()
private getDocumentService: GetAttachment;
@Inject()
private uploadPipelineService: AttachmentUploadPipeline;
@Inject()
private linkDocumentService: LinkAttachment;
@@ -26,6 +31,14 @@ export class AttachmentsApplication {
@Inject()
private getPresignedUrlService: getAttachmentPresignedUrl;
/**
* Express middleware for uploading attachments to an S3 bucket.
* @returns {Multer}
*/
get uploadPipeline(): Multer {
return this.uploadPipelineService.uploadPipeline();
}
/**
* Saves the metadata of uploaded document to S3 on database.
* @param {number} tenantId

View File

@@ -1,38 +1,12 @@
import multer from 'multer';
import type { Multer } from 'multer';
import type { Multer } from 'multer'
import multerS3 from 'multer-s3';
import { s3 } from '@/lib/S3/S3';
import { Service } from 'typedi';
import config from '@/config';
import { NextFunction, Request, Response } from 'express';
@Service()
export class AttachmentUploadPipeline {
/**
* Middleware to ensure that S3 configuration is properly set before proceeding.
* This function checks if the necessary S3 configuration keys are present and throws an error if any are missing.
*
* @param req The HTTP request object.
* @param res The HTTP response object.
* @param next The callback to pass control to the next middleware function.
*/
public validateS3Configured(req: Request, res: Response, next: NextFunction) {
if (
!config.s3.region ||
!config.s3.accessKeyId ||
!config.s3.secretAccessKey
) {
const missingKeys = [];
if (!config.s3.region) missingKeys.push('region');
if (!config.s3.accessKeyId) missingKeys.push('accessKeyId');
if (!config.s3.secretAccessKey) missingKeys.push('secretAccessKey');
const missing = missingKeys.join(', ');
throw new Error(`S3 configuration error: Missing ${missing}`);
}
next();
}
/**
* Express middleware for uploading attachments to an S3 bucket.
* It utilizes the multer middleware for handling multipart/form-data, specifically for file uploads.

View File

@@ -3,11 +3,7 @@ import { Inject, Service } from 'typedi';
import bluebird from 'bluebird';
import { entries, groupBy } from 'lodash';
import { CreateAccount } from '@/services/Accounts/CreateAccount';
import {
IAccountCreateDTO,
PlaidAccount,
PlaidTransaction,
} from '@/interfaces';
import { PlaidAccount, PlaidTransaction } from '@/interfaces';
import {
transformPlaidAccountToCreateAccount,
transformPlaidTrxsToCashflowCreate,
@@ -15,7 +11,6 @@ import {
import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { Knex } from 'knex';
const CONCURRENCY_ASYNC = 10;
@@ -33,35 +28,6 @@ export class PlaidSyncDb {
@Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction;
/**
* Syncs the Plaid bank account.
* @param {number} tenantId
* @param {IAccountCreateDTO} createBankAccountDTO
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public async syncBankAccount(
tenantId: number,
createBankAccountDTO: IAccountCreateDTO,
trx?: Knex.Transaction
) {
const { Account } = this.tenancy.models(tenantId);
const plaidAccount = await Account.query().findOne(
'plaidAccountId',
createBankAccountDTO.plaidAccountId
);
// Can't continue if the Plaid account is already created.
if (plaidAccount) {
return;
}
await this.createAccountService.createAccount(
tenantId,
createBankAccountDTO,
trx,
{ ignoreUniqueName: true }
);
}
/**
* Syncs the plaid accounts to the system accounts.
* @param {number} tenantId Tenant ID.
@@ -71,8 +37,7 @@ export class PlaidSyncDb {
public async syncBankAccounts(
tenantId: number,
plaidAccounts: PlaidAccount[],
institution: any,
trx?: Knex.Transaction
institution: any
): Promise<void> {
const transformToPlaidAccounts =
transformPlaidAccountToCreateAccount(institution);
@@ -82,7 +47,7 @@ export class PlaidSyncDb {
await bluebird.map(
accountCreateDTOs,
(createAccountDTO: any) =>
this.syncBankAccount(tenantId, createAccountDTO, trx),
this.createAccountService.createAccount(tenantId, createAccountDTO),
{ concurrency: CONCURRENCY_ASYNC }
);
}
@@ -96,16 +61,15 @@ export class PlaidSyncDb {
public async syncAccountTranactions(
tenantId: number,
plaidAccountId: number,
plaidTranasctions: PlaidTransaction[],
trx?: Knex.Transaction
plaidTranasctions: PlaidTransaction[]
): Promise<void> {
const { Account } = this.tenancy.models(tenantId);
const cashflowAccount = await Account.query(trx)
const cashflowAccount = await Account.query()
.findOne({ plaidAccountId })
.throwIfNotFound();
const openingEquityBalance = await Account.query(trx).findOne(
const openingEquityBalance = await Account.query().findOne(
'slug',
'opening-balance-equity'
);
@@ -123,8 +87,7 @@ export class PlaidSyncDb {
(uncategoriedDTO) =>
this.cashflowApp.createUncategorizedTransaction(
tenantId,
uncategoriedDTO,
trx
uncategoriedDTO
),
{ concurrency: 1 }
);
@@ -137,8 +100,7 @@ export class PlaidSyncDb {
*/
public async syncAccountsTransactions(
tenantId: number,
plaidAccountsTransactions: PlaidTransaction[],
trx?: Knex.Transaction
plaidAccountsTransactions: PlaidTransaction[]
): Promise<void> {
const groupedTrnsxByAccountId = entries(
groupBy(plaidAccountsTransactions, 'account_id')
@@ -149,8 +111,7 @@ export class PlaidSyncDb {
return this.syncAccountTranactions(
tenantId,
plaidAccountId,
plaidTransactions,
trx
plaidTransactions
);
},
{ concurrency: CONCURRENCY_ASYNC }
@@ -163,12 +124,11 @@ export class PlaidSyncDb {
*/
public async syncRemoveTransactions(
tenantId: number,
plaidTransactionsIds: string[],
trx?: Knex.Transaction
plaidTransactionsIds: string[]
) {
const { CashflowTransaction } = this.tenancy.models(tenantId);
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
const cashflowTransactions = await CashflowTransaction.query().whereIn(
'plaidTransactionId',
plaidTransactionsIds
);
@@ -180,8 +140,7 @@ export class PlaidSyncDb {
(transactionId: number) =>
this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
transactionId,
trx
transactionId
),
{ concurrency: CONCURRENCY_ASYNC }
);
@@ -196,12 +155,11 @@ export class PlaidSyncDb {
public async syncTransactionsCursor(
tenantId: number,
plaidItemId: string,
lastCursor: string,
trx?: Knex.Transaction
lastCursor: string
) {
const { PlaidItem } = this.tenancy.models(tenantId);
await PlaidItem.query(trx).findOne({ plaidItemId }).patch({ lastCursor });
await PlaidItem.query().findOne({ plaidItemId }).patch({ lastCursor });
}
/**
@@ -211,16 +169,13 @@ export class PlaidSyncDb {
*/
public async updateLastFeedsUpdatedAt(
tenantId: number,
plaidAccountIds: string[],
trx?: Knex.Transaction
plaidAccountIds: string[]
) {
const { Account } = this.tenancy.models(tenantId);
await Account.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
lastFeedsUpdatedAt: new Date(),
});
await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
lastFeedsUpdatedAt: new Date(),
});
}
/**
@@ -232,15 +187,12 @@ export class PlaidSyncDb {
public async updateAccountsFeedsActive(
tenantId: number,
plaidAccountIds: string[],
isFeedsActive: boolean = true,
trx?: Knex.Transaction
isFeedsActive: boolean = true
) {
const { Account } = this.tenancy.models(tenantId);
await Account.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
isFeedsActive,
});
await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
isFeedsActive,
});
}
}

View File

@@ -3,8 +3,6 @@ import { Inject, Service } from 'typedi';
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
import { PlaidSyncDb } from './PlaidSyncDB';
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
@Service()
export class PlaidUpdateTransactions {
@@ -14,40 +12,12 @@ export class PlaidUpdateTransactions {
@Inject()
private plaidSync: PlaidSyncDb;
@Inject()
private uow: UnitOfWork;
/**
* Handles sync the Plaid item to Bigcaptial under UOW.
* @param {number} tenantId
* @param {number} plaidItemId
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
* Handles the fetching and storing of new, modified, or removed transactions
* @param {number} tenantId Tenant ID.
* @param {string} plaidItemId the Plaid ID for the item.
*/
public async updateTransactions(tenantId: number, plaidItemId: string) {
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {
return this.updateTransactionsWork(tenantId, plaidItemId, trx);
});
}
/**
* Handles the fetching and storing the following:
* - New, modified, or removed transactions.
* - New bank accounts.
* - Last accounts feeds updated at.
* - Turn on the accounts feed flag.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactionsWork(
tenantId: number,
plaidItemId: string,
trx?: Knex.Transaction
): Promise<{
addedCount: number;
modifiedCount: number;
removedCount: number;
}> {
// Fetch new transactions from plaid api.
const { added, modified, removed, cursor, accessToken } =
await this.fetchTransactionUpdates(tenantId, plaidItemId);
@@ -59,42 +29,28 @@ export class PlaidUpdateTransactions {
} = await plaidInstance.accountsGet(request);
const plaidAccountsIds = accounts.map((a) => a.account_id);
const {
data: { institution },
} = await plaidInstance.institutionsGetById({
institution_id: item.institution_id,
country_codes: ['US', 'UK'],
});
// Sync bank accounts.
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution, trx);
// Sync bank account transactions.
// Update the DB.
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution);
await this.plaidSync.syncAccountsTransactions(
tenantId,
added.concat(modified),
trx
);
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
// Sync transactions cursor.
await this.plaidSync.syncTransactionsCursor(
tenantId,
plaidItemId,
cursor,
trx
added.concat(modified)
);
await this.plaidSync.syncRemoveTransactions(tenantId, removed);
await this.plaidSync.syncTransactionsCursor(tenantId, plaidItemId, cursor);
// Update the last feeds updated at of the updated accounts.
await this.plaidSync.updateLastFeedsUpdatedAt(
tenantId,
plaidAccountsIds,
trx
);
await this.plaidSync.updateLastFeedsUpdatedAt(tenantId, plaidAccountsIds);
// Turn on the accounts feeds flag.
await this.plaidSync.updateAccountsFeedsActive(
tenantId,
plaidAccountsIds,
true,
trx
);
await this.plaidSync.updateAccountsFeedsActive(tenantId, plaidAccountsIds);
return {
addedCount: added.length,
modifiedCount: modified.length,

View File

@@ -42,12 +42,7 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
): CreateUncategorizedTransactionDTO => {
return {
date: plaidTranasction.date,
// Plaid: Positive values when money moves out of the account; negative values
// when money moves in. For example, debit card purchases are positive;
// credit card payments, direct deposits, and refunds are negative.
amount: -1 * plaidTranasction.amount,
amount: plaidTranasction.amount,
description: plaidTranasction.name,
payee: plaidTranasction.payment_meta?.payee,
currencyCode: plaidTranasction.iso_currency_code,

View File

@@ -1,4 +1,3 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { DeleteCashflowTransaction } from './DeleteCashflowTransactionService';
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
@@ -120,13 +119,11 @@ export class CashflowApplication {
*/
public createUncategorizedTransaction(
tenantId: number,
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO
) {
return this.createUncategorizedTransactionService.create(
tenantId,
createUncategorizedTransactionDTO,
trx
createUncategorizedTransactionDTO
);
}

View File

@@ -12,6 +12,7 @@ import { Knex } from 'knex';
import { transformCategorizeTransToCashflow } from './utils';
import { CommandCashflowValidator } from './CommandCasflowValidator';
import NewCashflowTransactionService from './NewCashflowTransactionService';
import { TransferAuthorizationGuaranteeDecision } from 'plaid';
@Service()
export class CategorizeCashflowTransaction {

View File

@@ -30,8 +30,7 @@ export class DeleteCashflowTransaction {
*/
public deleteCashflowTransaction = async (
tenantId: number,
cashflowTransactionId: number,
trx?: Knex.Transaction
cashflowTransactionId: number
): Promise<{ oldCashflowTransaction: ICashflowTransaction }> => {
const { CashflowTransaction, CashflowTransactionLine } =
this.tenancy.models(tenantId);
@@ -44,44 +43,34 @@ export class DeleteCashflowTransaction {
this.throwErrorIfTransactionNotFound(oldCashflowTransaction);
// Starting database transaction.
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onCashflowTransactionDelete` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionDeleting,
{
trx,
tenantId,
oldCashflowTransaction,
} as ICommandCashflowDeletingPayload
);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCashflowTransactionDelete` event.
await this.eventPublisher.emitAsync(events.cashflow.onTransactionDeleting, {
trx,
tenantId,
oldCashflowTransaction,
} as ICommandCashflowDeletingPayload);
// Delete cashflow transaction associated lines first.
await CashflowTransactionLine.query(trx)
.where('cashflow_transaction_id', cashflowTransactionId)
.delete();
// Delete cashflow transaction associated lines first.
await CashflowTransactionLine.query(trx)
.where('cashflow_transaction_id', cashflowTransactionId)
.delete();
// Delete cashflow transaction.
await CashflowTransaction.query(trx)
.findById(cashflowTransactionId)
.delete();
// Delete cashflow transaction.
await CashflowTransaction.query(trx)
.findById(cashflowTransactionId)
.delete();
// Triggers `onCashflowTransactionDeleted` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionDeleted,
{
trx,
tenantId,
cashflowTransactionId,
oldCashflowTransaction,
} as ICommandCashflowDeletedPayload
);
// Triggers `onCashflowTransactionDeleted` event.
await this.eventPublisher.emitAsync(events.cashflow.onTransactionDeleted, {
trx,
tenantId,
cashflowTransactionId,
oldCashflowTransaction,
} as ICommandCashflowDeletedPayload);
return { oldCashflowTransaction };
},
trx
);
return { oldCashflowTransaction };
});
};
/**

View File

@@ -68,11 +68,7 @@ export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: {
type: 'OtherExpense',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [
ACCOUNT_TYPE.EXPENSE,
ACCOUNT_TYPE.OTHER_EXPENSE,
ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
],
creditType: [ACCOUNT_TYPE.EXPENSE, ACCOUNT_TYPE.OTHER_EXPENSE],
},
};

View File

@@ -1,4 +1,4 @@
import { upperFirst, camelCase } from 'lodash';
import { upperFirst, camelCase, omit } from 'lodash';
import {
CASHFLOW_TRANSACTION_TYPE,
CASHFLOW_TRANSACTION_TYPE_META,
@@ -6,6 +6,7 @@ import {
} from './constants';
import {
ICashflowNewCommandDTO,
ICashflowTransaction,
ICategorizeCashflowTransactioDTO,
IUncategorizedCashflowTransaction,
} from '@/interfaces';
@@ -41,8 +42,8 @@ export const getCashflowAccountTransactionsTypes = () => {
/**
* Tranasformes the given uncategorized transaction and categorized DTO
* to cashflow create DTO.
* @param {IUncategorizedCashflowTransaction} uncategorizeModel
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO
* @param {IUncategorizedCashflowTransaction} uncategorizeModel
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO
* @returns {ICashflowNewCommandDTO}
*/
export const transformCategorizeTransToCashflow = (
@@ -61,7 +62,6 @@ export const transformCategorizeTransToCashflow = (
transactionNumber: categorizeDTO.transactionNumber,
transactionType: categorizeDTO.transactionType,
uncategorizedTransactionId: uncategorizeModel.id,
branchId: categorizeDTO?.branchId,
publish: true,
};
};

View File

@@ -108,28 +108,17 @@ export default class ResourceService {
const $hasFields = (field) =>
'undefined' !== typeof field.fields ? field : undefined;
const $ColumnHasColumns = (column) =>
const $hasColumns = (column) =>
'undefined' !== typeof column.columns ? column : undefined;
const $hasColumns = (columns) =>
'undefined' !== typeof columns ? columns : undefined;
const naviagations = [
['fields', qim.$each, 'name'],
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, 'name'],
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
['columns', $hasColumns, qim.$each, 'name'],
[
'columns',
$hasColumns,
qim.$each,
$ColumnHasColumns,
'columns',
qim.$each,
'name',
],
['columns', qim.$each, 'name'],
['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'],
];
return this.i18nService.i18nApply(naviagations, meta, tenantId);
}

View File

@@ -2,7 +2,6 @@ import { Service } from 'typedi';
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
import { SystemUser } from '@/system/models';
import { configureLemonSqueezy } from './utils';
import config from '@/config';
@Service()
export class LemonSqueezyService {
@@ -29,7 +28,7 @@ export class LemonSqueezyService {
},
productOptions: {
enabledVariants: [variantId],
redirectUrl: config.lemonSqueezy.redirectTo,
redirectUrl: `http://localhost:4000/dashboard/billing/`,
receiptButtonText: 'Go to Dashboard',
receiptThankYouNote: 'Thank you for signing up to Lemon Stand!',
},

View File

@@ -1,18 +0,0 @@
// @ts-nocheck
import { FSuggest } from '../Forms';
interface BranchSuggestFieldProps {
items: any[];
}
export function BranchSuggestField({ ...props }: BranchSuggestFieldProps) {
return (
<FSuggest
valueAccessor={'id'}
labelAccessor={'code'}
textAccessor={'name'}
inputProps={{ placeholder: 'Select a branch' }}
{...props}
/>
);
}

View File

@@ -50,6 +50,7 @@ import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/Inv
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
import { ConnectBankDialog } from '@/containers/CashFlow/ConnectBankDialog';
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
/**
@@ -96,6 +97,7 @@ export default function DialogsContainer() {
<NotifyPaymentReceiveViaSMSDialog
dialogName={DialogsName.NotifyPaymentViaForm}
/>
<BadDebtDialog dialogName={DialogsName.BadDebtForm} />
<SMSMessageDialog dialogName={DialogsName.SMSMessageForm} />
<RefundCreditNoteDialog dialogName={DialogsName.RefundCreditNote} />
@@ -146,6 +148,8 @@ export default function DialogsContainer() {
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ConnectBankDialog dialogName={DialogsName.ConnectBankCreditCard} />
<ExportDialog dialogName={DialogsName.Export} />
</div>
);

View File

@@ -15,7 +15,6 @@ import {
FeatureCan,
} from '@/components';
import { useRefreshCashflowAccounts } from '@/hooks/query';
import { useOpenPlaidConnect } from '@/hooks/utils/useOpenPlaidConnect';
import { CashflowAction, AbilitySubject } from '@/constants/abilityOption';
import withDialogActions from '@/containers/Dialog/withDialogActions';
@@ -40,9 +39,6 @@ function CashFlowAccountsActionsBar({
}) {
const { refresh } = useRefreshCashflowAccounts();
// Opens the Plaid popup.
const { openPlaidAsync, isPlaidLoading } = useOpenPlaidConnect();
// Handle refresh button click.
const handleRefreshBtnClick = () => {
refresh();
@@ -68,7 +64,7 @@ function CashFlowAccountsActionsBar({
};
// Handle connect button click.
const handleConnectToBank = () => {
openPlaidAsync();
openDialog(DialogsName.ConnectBankCreditCard);
};
return (
@@ -120,7 +116,6 @@ function CashFlowAccountsActionsBar({
className={Classes.MINIMAL}
text={'Connect to Bank / Credit Card'}
onClick={handleConnectToBank}
disabled={isPlaidLoading}
/>
<NavbarDivider />
</FeatureCan>

View File

@@ -1,6 +1,5 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import { first } from 'lodash';
import React from 'react';
import { DrawerHeaderContent, DrawerLoading } from '@/components';
import { DRAWERS } from '@/constants/drawers';
import {
@@ -35,12 +34,6 @@ function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
isLoading: isUncategorizedTransactionLoading,
} = useUncategorizedTransaction(uncategorizedTransactionId);
// Retrieves the primary branch.
const primaryBranch = useMemo(
() => branches?.find((b) => b.primary) || first(branches),
[branches],
);
const provider = {
uncategorizedTransactionId,
uncategorizedTransaction,
@@ -49,7 +42,6 @@ function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
accounts,
isBranchesLoading,
isAccountsLoading,
primaryBranch,
};
const isLoading =
isBranchesLoading || isUncategorizedTransactionLoading || isAccountsLoading;

View File

@@ -1,22 +0,0 @@
// @ts-nocheck
import { FFormGroup, FeatureCan } from '@/components';
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
import { Features } from '@/constants';
import { BranchSuggestField } from '@/components/Branches/BranchSuggestField_';
export function CategorizeTransactionBranchField() {
const { branches } = useCategorizeTransactionBoot();
return (
<FFormGroup name={'branchId'} label={'Branch'} fastField inline>
<FeatureCan feature={Features.Branches}>
<BranchSuggestField
name={'branchId'}
items={branches}
popoverProps={{ minimal: true }}
fill
/>
</FeatureCan>
</FFormGroup>
);
}

View File

@@ -24,11 +24,8 @@ function CategorizeTransactionFormRoot({
// #withDrawerActions
closeDrawer,
}) {
const {
uncategorizedTransactionId,
uncategorizedTransaction,
primaryBranch,
} = useCategorizeTransactionBoot();
const { uncategorizedTransactionId, uncategorizedTransaction } =
useCategorizeTransactionBoot();
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
// Callbacks handles form submit.
@@ -40,28 +37,18 @@ function CategorizeTransactionFormRoot({
.then(() => {
setSubmitting(false);
closeDrawer(DRAWERS.CATEGORIZE_TRANSACTION);
AppToaster.show({
message: 'The uncategorized transaction has been categorized.',
intent: Intent.SUCCESS,
});
})
.catch((err) => {
.catch(() => {
setSubmitting(false);
if (
err.response.data?.errors?.some(
(e) => e.type === 'BRANCH_ID_REQUIRED',
)
) {
setErrors({
branchId: 'The branch is required.',
});
} else {
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.DANGER,
});
}
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.DANGER,
});
});
};
// Form initial values in create and edit mode.
@@ -73,9 +60,6 @@ function CategorizeTransactionFormRoot({
* as well.
*/
...transformToCategorizeForm(uncategorizedTransaction),
/** Assign the primary branch id as default value. */
branchId: primaryBranch?.id || null,
};
return (

View File

@@ -8,7 +8,6 @@ import {
FTextArea,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOtherIncome() {
const { accounts } = useCategorizeTransactionBoot();
@@ -69,8 +68,6 @@ export default function CategorizeTransactionOtherIncome() {
fill={true}
/>
</FFormGroup>
<CategorizeTransactionBranchField />
</>
);
}

View File

@@ -8,7 +8,6 @@ import {
FTextArea,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOwnerContribution() {
const { accounts } = useCategorizeTransactionBoot();
@@ -64,8 +63,6 @@ export default function CategorizeTransactionOwnerContribution() {
<FFormGroup name={'description'} label={'Description'} fastField inline>
<FTextArea name={'description'} growVertically large fill />
</FFormGroup>
<CategorizeTransactionBranchField />
</>
);
}

View File

@@ -8,7 +8,6 @@ import {
FTextArea,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionTransferFrom() {
const { accounts } = useCategorizeTransactionBoot();
@@ -48,7 +47,7 @@ export default function CategorizeTransactionTransferFrom() {
inline
>
<AccountsSelect
name={'creditAccountId'}
name={'to_account_id'}
items={accounts}
filterByRootTypes={['asset']}
fastField
@@ -69,8 +68,6 @@ export default function CategorizeTransactionTransferFrom() {
fill={true}
/>
</FFormGroup>
<CategorizeTransactionBranchField />
</>
);
}

View File

@@ -8,7 +8,6 @@ import {
FTextArea,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOtherExpense() {
const { accounts } = useCategorizeTransactionBoot();
@@ -69,8 +68,6 @@ export default function CategorizeTransactionOtherExpense() {
fill={true}
/>
</FFormGroup>
<CategorizeTransactionBranchField />
</>
);
}

View File

@@ -8,7 +8,6 @@ import {
FTextArea,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOwnerDrawings() {
const { accounts } = useCategorizeTransactionBoot();
@@ -69,8 +68,6 @@ export default function CategorizeTransactionOwnerDrawings() {
fill={true}
/>
</FFormGroup>
<CategorizeTransactionBranchField />
</>
);
}

View File

@@ -8,7 +8,6 @@ import {
FTextArea,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionToAccount() {
const { accounts } = useCategorizeTransactionBoot();
@@ -50,7 +49,7 @@ export default function CategorizeTransactionToAccount() {
<AccountsSelect
name={'creditAccountId'}
items={accounts}
filterByRootTypes={['asset']}
filterByRootTypes={['assset']}
fastField
fill
allowCreate
@@ -69,8 +68,6 @@ export default function CategorizeTransactionToAccount() {
fill={true}
/>
</FFormGroup>
<CategorizeTransactionBranchField />
</>
);
}

View File

@@ -11,7 +11,6 @@ export const defaultInitialValues = {
transactionType: '',
referenceNo: '',
description: '',
branchId: '',
};
export const transformToCategorizeForm = (uncategorizedTransaction) => {

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const ConnectBankDialogBody = React.lazy(
() => import('./ConnectBankDialogBody'),
);
/**
* Connect bank dialog.
*/
function ConnectBankDialogRoot({ dialogName, payload = {}, isOpen }) {
return (
<Dialog
name={dialogName}
title={'Securly connect your bank or credit card.'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
>
<DialogSuspense>
<ConnectBankDialogBody dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export const ConnectBankDialog = compose(withDialogRedux())(
ConnectBankDialogRoot,
);

View File

@@ -0,0 +1,61 @@
// @ts-nocheck
import * as R from 'ramda';
import { Form, Formik, FormikHelpers } from 'formik';
import classNames from 'classnames';
import { ConnectBankDialogContent } from './ConnectBankDialogContent';
import { useGetPlaidLinkToken } from '@/hooks/query';
import { useSetBankingPlaidToken } from '@/hooks/state/banking';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { CLASSES } from '@/constants';
import { AppToaster } from '@/components';
import { Intent } from '@blueprintjs/core';
import { DialogsName } from '@/constants/dialogs';
const initialValues: ConnectBankDialogForm = {
serviceProvider: 'plaid',
};
interface ConnectBankDialogForm {
serviceProvider: 'plaid';
}
function ConnectBankDialogBodyRoot({
// #withDialogActions
closeDialog,
}) {
const { mutateAsync: getPlaidLinkToken } = useGetPlaidLinkToken();
const setPlaidId = useSetBankingPlaidToken();
// Handles the form submitting.
const handleSubmit = (
values: ConnectBankDialogForm,
{ setSubmitting }: FormikHelpers<ConnectBankDialogForm>,
) => {
setSubmitting(true);
getPlaidLinkToken()
.then((res) => {
setSubmitting(false);
closeDialog(DialogsName.ConnectBankCreditCard);
setPlaidId(res.data.link_token);
})
.catch(() => {
setSubmitting(false);
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
return (
<div className={classNames(CLASSES.DIALOG_BODY)}>
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<Form>
<ConnectBankDialogContent />
</Form>
</Formik>
</div>
);
}
export default R.compose(withDialogActions)(ConnectBankDialogBodyRoot);

View File

@@ -0,0 +1,48 @@
// @ts-nocheck
import styled from 'styled-components';
import { Stack } from '@/components';
import { TellerIcon } from '../Icons/TellerIcon';
import { YodleeIcon } from '../Icons/YodleeIcon';
import { PlaidIcon } from '../Icons/PlaidIcon';
import { BankServiceCard } from './ConnectBankServiceCard';
const TopDesc = styled('p')`
margin-bottom: 20px;
color: #5f6b7c;
`;
export function ConnectBankDialogContent() {
return (
<div>
<TopDesc>
Connect your bank accounts and fetch the bank transactions using
one of our supported third-party service providers.
</TopDesc>
<Stack>
<BankServiceCard
title={'Plaid (US, UK & Canada)'}
icon={<PlaidIcon />}
>
Plaid gives the connection to 12,000 financial institutions across US, UK and Canada.
</BankServiceCard>
<BankServiceCard
title={'Teller (US) — Soon'}
icon={<TellerIcon />}
disabled
>
Connect instantly with more than 5,000 financial institutions across US.
</BankServiceCard>
<BankServiceCard
title={'Yodlee (Global) — Soon'}
icon={<YodleeIcon />}
disabled
>
Connect instantly with a global network of financial institutions.
</BankServiceCard>
</Stack>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import styled from 'styled-components';
import { Group } from '@/components';
const BankServiceIcon = styled('div')`
height: 40px;
width: 40px;
border: 1px solid #c8cad0;
border-radius: 3px;
display: flex;
svg {
margin: auto;
}
`;
const BankServiceContent = styled(`div`)`
flex: 1 0;
`;
const BankServiceCardRoot = styled('button')`
border-radius: 3px;
border: 1px solid #c8cad0;
transition: all 0.1s ease-in-out;
background: transparent;
text-align: inherit;
padding: 14px;
&:not(:disabled) {
cursor: pointer;
}
&:hover:not(:disabled) {
border-color: #0153cc;
}
&:disabled {
background: #f9fdff;
}
`;
const BankServiceTitle = styled(`h3`)`
font-weight: 600;
font-size: 14px;
color: #2d333d;
`;
const BankServiceDesc = styled('p')`
margin-top: 4px;
margin-bottom: 6px;
font-size: 13px;
color: #738091;
`;
interface BankServiceCardProps {
title: string;
children: React.ReactNode;
disabled?: boolean;
icon: React.ReactNode;
}
export function BankServiceCard({
title,
children,
icon,
disabled,
}: BankServiceCardProps) {
return (
<BankServiceCardRoot disabled={disabled}>
<Group>
<BankServiceIcon>{icon}</BankServiceIcon>
<BankServiceContent>
<BankServiceTitle>{title}</BankServiceTitle>
<BankServiceDesc>{children}</BankServiceDesc>
</BankServiceContent>
</Group>
</BankServiceCardRoot>
);
}

View File

@@ -0,0 +1 @@
export * from './ConnectBankDialog';

View File

@@ -1,25 +0,0 @@
import { useCallback } from 'react';
import { useSetBankingPlaidToken } from '../state/banking';
import { AppToaster } from '@/components';
import { useGetPlaidLinkToken } from '../query';
import { Intent } from '@blueprintjs/core';
export const useOpenPlaidConnect = () => {
const { mutateAsync: getPlaidLinkToken, isLoading } = useGetPlaidLinkToken();
const setPlaidId = useSetBankingPlaidToken();
const openPlaidAsync = useCallback(() => {
return getPlaidLinkToken()
.then((res) => {
setPlaidId(res.data.link_token);
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
}, [getPlaidLinkToken, setPlaidId]);
return { openPlaidAsync, isPlaidLoading: isLoading };
};

55
pnpm-lock.yaml generated
View File

@@ -58,7 +58,7 @@ importers:
version: 0.8.8
'@types/knex':
specifier: ^0.16.1
version: 0.16.1(mysql2@1.7.0)(mysql@2.18.1)
version: 0.16.1(mysql2@3.9.8)(mysql@2.18.1)
'@types/mathjs':
specifier: ^6.0.12
version: 6.0.12
@@ -181,7 +181,7 @@ importers:
version: 8.5.1
knex:
specifier: ^0.95.15
version: 0.95.15(mysql2@1.7.0)(mysql@2.18.1)
version: 0.95.15(mysql2@3.9.8)(mysql@2.18.1)
knex-cleaner:
specifier: ^1.3.0
version: 1.3.1
@@ -234,8 +234,8 @@ importers:
specifier: ^2.17.1
version: 2.18.1
mysql2:
specifier: ^1.6.5
version: 1.7.0
specifier: ^3.9.8
version: 3.9.8
newrelic:
specifier: ^11.15.0
version: 11.17.0
@@ -6480,11 +6480,11 @@ packages:
'@types/node': 14.18.63
dev: false
/@types/knex@0.16.1(mysql2@1.7.0)(mysql@2.18.1):
/@types/knex@0.16.1(mysql2@3.9.8)(mysql@2.18.1):
resolution: {integrity: sha512-54gWD1HWwdVx5iLHaJ1qxH3I6KyBsj5fFqzRpXFn7REWiEB2jwspeVCombNsocSrqPd7IRPqKrsIME7/cD+TFQ==}
deprecated: This is a stub types definition. knex provides its own type definitions, so you do not need this installed.
dependencies:
knex: 0.95.15(mysql2@1.7.0)(mysql@2.18.1)
knex: 0.95.15(mysql2@3.9.8)(mysql@2.18.1)
transitivePeerDependencies:
- mysql
- mysql2
@@ -10788,6 +10788,11 @@ packages:
engines: {node: '>=0.10'}
dev: false
/denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
dev: false
/depd@1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
engines: {node: '>= 0.6'}
@@ -14214,13 +14219,6 @@ packages:
dependencies:
safer-buffer: 2.1.2
/iconv-lite@0.5.2:
resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -16531,7 +16529,7 @@ packages:
dependencies:
bluebird: 3.7.2
glob: 7.2.3
knex: 0.95.15(mysql2@1.7.0)(mysql@2.18.1)
knex: 0.95.15(mysql2@3.9.8)(mysql@2.18.1)
lodash: 4.17.21
dev: false
@@ -16541,7 +16539,7 @@ packages:
lodash: 4.17.21
dev: true
/knex@0.95.15(mysql2@1.7.0)(mysql@2.18.1):
/knex@0.95.15(mysql2@3.9.8)(mysql@2.18.1):
resolution: {integrity: sha512-Loq6WgHaWlmL2bfZGWPsy4l8xw4pOE+tmLGkPG0auBppxpI0UcK+GYCycJcqz9W54f2LiGewkCVLBm3Wq4ur/w==}
engines: {node: '>=10'}
hasBin: true
@@ -16575,7 +16573,7 @@ packages:
interpret: 2.2.0
lodash: 4.17.21
mysql: 2.18.1
mysql2: 1.7.0
mysql2: 3.9.8
pg-connection-string: 2.5.0
rechoir: 0.7.0
resolve-from: 5.0.0
@@ -17089,10 +17087,6 @@ packages:
'@sinonjs/commons': 1.8.6
dev: true
/long@4.0.0:
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
dev: false
/long@5.2.3:
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
dev: false
@@ -17147,6 +17141,11 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
/lru-cache@8.0.5:
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
engines: {node: '>=16.14'}
dev: false
/luxon@1.28.1:
resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==}
dev: false
@@ -17382,6 +17381,7 @@ packages:
/memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
requiresBuild: true
dev: false
/memorystream@0.3.1:
@@ -17990,15 +17990,15 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dev: true
/mysql2@1.7.0:
resolution: {integrity: sha512-xTWWQPjP5rcrceZQ7CSTKR/4XIDeH/cRkNH/uzvVGQ7W5c7EJ0dXeJUusk7OKhIoHj7uFKUxDVSCfLIl+jluog==}
/mysql2@3.9.8:
resolution: {integrity: sha512-+5JKNjPuks1FNMoy9TYpl77f+5frbTklz7eb3XDwbpsERRLEeXiW2PDEkakYF50UuKU2qwfGnyXpKYvukv8mGA==}
engines: {node: '>= 8.0'}
dependencies:
denque: 1.5.1
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.5.2
long: 4.0.0
lru-cache: 5.1.1
iconv-lite: 0.6.3
long: 5.2.3
lru-cache: 8.0.5
named-placeholders: 1.1.3
seq-queue: 0.0.5
sqlstring: 2.3.3
@@ -18866,7 +18866,7 @@ packages:
ajv: 8.13.0
ajv-formats: 2.1.1(ajv@8.13.0)
db-errors: 0.2.3
knex: 0.95.15(mysql2@1.7.0)(mysql@2.18.1)
knex: 0.95.15(mysql2@3.9.8)(mysql@2.18.1)
dev: false
/oblivious-set@1.0.0:
@@ -23472,6 +23472,7 @@ packages:
/sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
requiresBuild: true
dependencies:
memory-pager: 1.5.0
dev: false