Compare commits

...

69 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
f92acbcbe0 fix: Getting the sheet columns in import sheet 2024-08-30 17:03:16 +02:00
Ahmed Bouhuolia
c986585cd9 Merge pull request #640 from bigcapitalhq/fix-typo-one-click-demo
fix: Typo one-click demo page
2024-08-30 00:15:29 +02:00
Ahmed Bouhuolia
250f0a30ef fix: Typo one-click demo page 2024-08-30 00:14:59 +02:00
Ahmed Bouhuolia
84b5e1adc1 Merge pull request #639 from bigcapitalhq/add-customer-type-to-customers
fix: Add customer type to customers resource
2024-08-29 22:50:20 +02:00
Ahmed Bouhuolia
2ab28370db fix: Add customer type to customers resource 2024-08-29 22:49:49 +02:00
Ahmed Bouhuolia
a88a525326 Merge pull request #638 from bigcapitalhq/use-standard-date-format-export
fix: use standard ISO 8601 format for exported data
2024-08-29 22:40:59 +02:00
Ahmed Bouhuolia
fce8e2c5a4 fix: use standard ISO 8601 format for exported data 2024-08-29 22:39:51 +02:00
Ahmed Bouhuolia
095608266c Merge pull request #632 from bigcapitalhq/split-lazy-loading
feat: Optimize loading perf. by splitting big chunks and lazy loading them
2024-08-29 21:27:40 +02:00
Ahmed Bouhuolia
0ec8aaa330 fix: delete unwanted files 2024-08-29 21:27:20 +02:00
Ahmed Bouhuolia
9fcb3ef77d feat: Add fallback spinner to authentication lazy-loaded pages 2024-08-29 21:19:36 +02:00
Ahmed Bouhuolia
dc61c57daf fix: Add spinner to preferences lazy loaded pages 2024-08-29 21:03:27 +02:00
Ahmed Bouhuolia
af284f3f6d feat: split the preferences pages 2024-08-29 20:49:08 +02:00
Ahmed Bouhuolia
c43123db76 Merge pull request #636 from bigcapitalhq/expand-export-page-size
fix: Expand the resources export page size limitation
2024-08-29 14:23:48 +02:00
Ahmed Bouhuolia
ebbcab3926 fix: Expand the resources export page size limitation 2024-08-29 14:22:45 +02:00
Ahmed Bouhuolia
a235f573c0 Merge pull request #635 from bigcapitalhq/fix-avoid-cost-job-import-preview
fix: Avoid running the cost job in import preview
2024-08-29 10:09:13 +02:00
Ahmed Bouhuolia
84a0b8f495 fix: re-schedule the jobs have date from the current moment 2024-08-29 10:05:38 +02:00
Ahmed Bouhuolia
b87321c897 fix: Avoid running the cost job in import preview 2024-08-28 22:15:15 +02:00
Ahmed Bouhuolia
fc6ebfea5c Debounce scheduling calculating items cost 2024-08-28 21:25:47 +02:00
Ahmed Bouhuolia
c9fe6d9b37 feat: Optimize loading perf. by spliting big chunks and lazy loading them 2024-08-26 22:51:40 +02:00
Ahmed Bouhuolia
161d60393a Merge pull request #629 from bigcapitalhq/details-subscription
fix: Add subscription plans offer text
2024-08-25 19:44:34 +02:00
Ahmed Bouhuolia
79413fa85e fix: Add subscription plans offer text 2024-08-25 19:43:54 +02:00
Ahmed Bouhuolia
58552c6c94 Merge pull request #628 from bigcapitalhq/fix-webapp-env-variables
fix: Make webapp package env variables dynamic
2024-08-25 18:21:55 +02:00
Ahmed Bouhuolia
2072e35cfa fix: Make webapp package env variables dynamic 2024-08-25 18:21:08 +02:00
Ahmed Bouhuolia
1eaac9d691 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-08-25 14:18:04 +02:00
Ahmed Bouhuolia
a916e8a0cb fix: syntax error 2024-08-25 14:17:23 +02:00
Ahmed Bouhuolia
a56f560036 Merge pull request #627 from bigcapitalhq/fix-style-tweeks-in-onboarding-page
fix: Style tweaks in onboarding page
2024-08-25 13:20:39 +02:00
Ahmed Bouhuolia
0fb886936c Merge pull request #626 from bigcapitalhq/disable-sms-service
fix: Disable sms service until Twilo integration
2024-08-25 13:20:14 +02:00
Ahmed Bouhuolia
670136916f fix: Style tweaks in onboarding page 2024-08-25 13:19:16 +02:00
Ahmed Bouhuolia
768297f137 fix: Disable sms service until Twilo integration 2024-08-25 13:07:07 +02:00
Ahmed Bouhuolia
ef505a0a62 Merge pull request #625 from bigcapitalhq/manual-journal-number-prefix
fix: Add prefix J-00001 to manual journals increments
2024-08-25 13:05:52 +02:00
Ahmed Bouhuolia
42d40620ec fix: add prefix J-00001 to manual journals increments 2024-08-25 13:05:28 +02:00
Ahmed Bouhuolia
959ef7a691 Merge branch 'listen-payment-webhooks' into develop 2024-08-24 21:52:04 +02:00
Ahmed Bouhuolia
60f03f534b fix(subscription): event TS types 2024-08-24 21:51:15 +02:00
Ahmed Bouhuolia
e44ebb700a Merge pull request #623 from bigcapitalhq/listen-payment-webhooks
fix: Listen to payment webhooks
2024-08-24 21:49:36 +02:00
Ahmed Bouhuolia
8e94c7a755 feat(subscription): invalidate subscription cache 2024-08-24 21:40:28 +02:00
Ahmed Bouhuolia
3a2ca36c07 Merge branch 'develop' into listen-payment-webhooks 2024-08-24 21:08:47 +02:00
Ahmed Bouhuolia
88ece74c8a Merge pull request #622 from wolone/develop
Fix: Syntax error caused error
2024-08-24 20:48:14 +02:00
Ahmed Bouhuolia
67a8610328 feat: cancel/resume LS subscriptions 2024-08-24 20:46:30 +02:00
Ahmed Bouhuolia
278d61ce61 fix: Listen to payment webhooks 2024-08-24 18:50:12 +02:00
wolone
e72d6ad6b8 Fix: Syntax error caused error 2024-08-24 23:33:53 +08:00
Ahmed Bouhuolia
bf66b31679 Merge pull request #590 from bigcapitalhq/filter-uncategorized-bank-transactions
feat(banking): Filter uncategorized bank transactions by date
2024-08-23 17:42:59 +02:00
Ahmed Bouhuolia
b4d426d2e8 feat: Filter uncategorized transactions by date 2024-08-23 17:40:05 +02:00
Ahmed Bouhuolia
7f7dd270e7 Merge pull request #619 from bigcapitalhq/change-banking-service-language
feat: change banking service language
2024-08-23 02:00:47 +02:00
Ahmed Bouhuolia
f6bad8fe30 fix: wip filter uncategorized transactions by date 2024-08-23 01:57:52 +02:00
Ahmed Bouhuolia
820b363f79 feat: change banking service language 2024-08-22 20:49:15 +02:00
Ahmed Bouhuolia
07740a51ef Merge pull request #616 from bigcapitalhq/one-click-demo-account
feat(ee): One-click demo account
2024-08-22 20:33:06 +02:00
Ahmed Bouhuolia
d15fb6fe19 chore: document http query 2024-08-22 20:32:48 +02:00
Ahmed Bouhuolia
5749ccec81 feat: seed more demo bank transactions 2024-08-22 19:50:31 +02:00
Ahmed Bouhuolia
4a99f6c0cf feat: one-click demo account 2024-08-22 19:21:23 +02:00
Ahmed Bouhuolia
59f480f9d5 feat: add more demo account seeders 2024-08-22 13:04:51 +02:00
Ahmed Bouhuolia
6cb9c919b5 Merge branch 'develop' into one-click-demo-account 2024-08-22 10:48:24 +02:00
Ahmed Bouhuolia
1062b65b5b feat: wip bank account transactions date filter 2024-08-22 01:03:03 +02:00
Ahmed Bouhuolia
bf3a70cabd Merge branch 'develop' into filter-uncategorized-bank-transactions 2024-08-22 00:12:37 +02:00
Ahmed Bouhuolia
f46cd28f87 Merge pull request #618 from bigcapitalhq/display-details-bank-account
fix: Some bank account details hidden
2024-08-21 21:22:15 +02:00
Ahmed Bouhuolia
dffcfe50aa fix: some bank account details hidden 2024-08-21 21:19:59 +02:00
Ahmed Bouhuolia
fac55efbc7 Merge pull request #617 from bigcapitalhq/fix-impoort-itemns
fix: Cannot import items income and cost accounts
2024-08-21 19:33:50 +02:00
Ahmed Bouhuolia
8b90ce5f6c fix: cannot import items income and cost accounts 2024-08-21 19:32:59 +02:00
Ahmed Bouhuolia
705b8da053 fix: protect the one-click demo accounts endpoints 2024-08-21 01:04:18 +02:00
Ahmed Bouhuolia
4a05ccc692 feat: add more seedders 2024-08-20 23:40:23 +02:00
Ahmed Bouhuolia
3200d65d90 feat: add demo account button on onboarding 2024-08-20 22:01:36 +02:00
Ahmed Bouhuolia
3f23038227 chore: comment 2024-08-20 18:49:39 +02:00
Ahmed Bouhuolia
408c807fc2 feat: import sheet files on initializing demo account 2024-08-20 18:30:21 +02:00
Ahmed Bouhuolia
d29079a8c5 fix: one-click demo account 2024-08-20 12:51:23 +02:00
Ahmed Bouhuolia
cca596b4a9 fix: one click demo 2024-08-19 21:21:39 +02:00
Ahmed Bouhuolia
b768f18294 chore: change subscription prices 2024-08-19 13:08:09 +02:00
Ahmed Bouhuolia
fed620505d feat: add one click endpoint 2024-08-19 12:10:38 +02:00
Ahmed Bouhuolia
a008aea3f3 feat(ee): one-click demo account 2024-08-19 12:08:58 +02:00
Ahmed Bouhuolia
25297bc191 chore: dump CHANGELOG 2024-08-18 20:56:10 +02:00
Ahmed Bouhuolia
df8b68fda6 feat(banking): Filter uncategorized bank transactions 2024-08-11 18:34:45 +02:00
162 changed files with 3195 additions and 473 deletions

View File

@@ -2,6 +2,27 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.19.4] - 18-08-2024
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
* fix: Transaction type and description do not show in general ledger. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/596
* fix: Refresh accounts and account transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/597
* fix: Typo payments made by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/598
* fix: Typo categories list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/600
* fix: Autofill the quick created customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/601
* fix: Remove views tabs from receipts list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/602
* fix: Typo payment receive messages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/599
* fix: Enhance Dropzone visual of accept and reject modes by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/603
* fix: Matching bank transactions should create associate payment transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/606
* fix: Change Dropzone title and subtitle by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/607
* fix: Inconsistance page size of paginated data tables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/604
* fix: Database connection lost error by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/611
* fix: Language typos by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/613
* Fix: Correctly display Date, Published At, and Created At in ExpenseDrawerHeader by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/612
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511

View File

@@ -37,6 +37,7 @@
"agendash": "^3.1.0",
"app-root-path": "^3.0.0",
"async": "^3.2.0",
"async-mutex": "^0.5.0",
"axios": "^1.6.0",
"babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3",

View File

@@ -46,6 +46,10 @@ export class ExcludeBankTransactionsController extends BaseController {
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
],
this.validationResult,
this.getExcludedBankTransactions.bind(this)

View File

@@ -21,6 +21,10 @@ export class RecognizedTransactionsController extends BaseController {
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().isFloat(),
],
this.validationResult,
this.getRecognizedTransactions.bind(this)

View File

@@ -84,6 +84,10 @@ export default class NewCashflowTransactionController extends BaseController {
param('id').exists().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
];
}

View File

@@ -0,0 +1,87 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication';
import config from '@/config';
@Service()
export class OneClickDemoController extends BaseController {
@Inject()
private oneClickDemoApp: OneClickDemoApplication;
/**
* Router constructor method.
*/
router() {
const router = Router();
// Protects the endpoints if the feature is not enabled.
const protectMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Add your protection logic here
if (config.oneClickDemoAccounts) {
next();
} else {
res.status(403).send({ message: 'Forbidden' });
}
};
router.post(
'/one_click',
protectMiddleware,
asyncMiddleware(this.oneClickDemo.bind(this))
);
router.post(
'/one_click_signin',
[body('demo_id').exists()],
this.validationResult,
protectMiddleware,
asyncMiddleware(this.oneClickSignIn.bind(this))
);
return router;
}
/**
* One-click demo application.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async oneClickDemo(req: Request, res: Response, next: NextFunction) {
try {
const data = await this.oneClickDemoApp.createOneClick();
return res.status(200).send({
data,
message: 'The one-click demo has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Sign-in to one-click demo account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async oneClickSignIn(
req: Request,
res: Response,
next: NextFunction
) {
const { demoId } = this.matchedBodyData(req);
try {
const data = await this.oneClickDemoApp.autoSignIn(demoId);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
}

View File

@@ -113,7 +113,7 @@ export class SubscriptionController extends BaseController {
const { tenantId } = req;
try {
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
await this.subscriptionApp.cancelSubscription(tenantId);
return res.status(200).send({
status: 200,

View File

@@ -35,7 +35,7 @@ export class Webhooks extends BaseController {
*/
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
const data = req.body;
const signature = req.headers['x-signature'] ?? '';
const signature = req.headers['x-signature'] as string ?? '';
const rawBody = req.rawBody;
try {

View File

@@ -63,6 +63,7 @@ import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
export default () => {
const app = Router();
@@ -80,6 +81,7 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())
// - Dashboard routes.
// ---------------------------

View File

@@ -1,11 +1,21 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import SettingsStore from '@/services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;
const Logger = Container.get('logger');
const settings = await initializeTenantSettings(tenantId);
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}
export const initializeTenantSettings = async (tenantId: number) => {
const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) {
@@ -18,10 +28,5 @@ export default async (req: Request, res: Response, next: NextFunction) => {
await settings.load();
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
return settings;
}

View File

@@ -4,6 +4,7 @@ import { Request } from 'express';
import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect';
import { Tenant } from '@/system/models';
export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant;
@@ -16,7 +17,7 @@ export default (req: Request, tenant: ITenant) => {
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils(req));
tenantContainer.set('i18n', injectI18nUtils());
const knexInstance = tenantServices.knex(tenantId);
const models = tenantServices.models(tenantId);
@@ -33,14 +34,35 @@ export default (req: Request, tenant: ITenant) => {
};
export const injectI18nUtils = (req) => {
const locale = req.getLocale();
const globalI18n = Container.get('i18n');
const locale = globalI18n.getLocale();
const direction = rtlDetect.getLangDir(locale);
return {
locale,
__: req.__,
__: globalI18n.__,
direction,
isRtl: direction === 'rtl',
isLtr: direction === 'ltr',
};
};
export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);
// Initialize the knex instance.
tenantsManager.setupKnexInstance(tenant);
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils());
tenantServices.knex(tenantId);
tenantServices.models(tenantId);
tenantServices.repositories(tenantId);
tenantServices.cache(tenantId);
};

View File

@@ -245,4 +245,12 @@ module.exports = {
loops: {
apiKey: process.env.LOOPS_API_KEY,
},
/**
* One-click demo accounts.
*/
oneClickDemoAccounts: {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
};

View File

@@ -15,6 +15,7 @@ export default class SeedSettings extends TenantSeeder {
// Manual journals settings.
{ group: 'manual_journals', key: 'next_number', value: '00001' },
{ group: 'manual_journals', key: 'number_prefix', value: 'J-' },
{ group: 'manual_journals', key: 'auto_increment', value: true },
// Sale invoices settings.

View File

@@ -7,7 +7,6 @@ export interface IRegisterDTO {
lastName: string;
email: string;
password: string;
organizationName: string;
}
export interface ILoginDTO {
@@ -77,6 +76,10 @@ export interface IAuthSendedResetPassword {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
oneClickDemo: {
enable: boolean;
demoUrl: string;
};
}
export interface IAuthSignUpVerifingEventPayload {

View File

@@ -167,11 +167,18 @@ export interface CategorizeTransactionAsExpenseDTO {
export interface IGetUncategorizedTransactionsQuery {
page?: number;
pageSize?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface IGetRecognizedTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}

View File

@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
export interface IImportFileCommitedEventPayload {
tenantId: number;
importId: number;
importId: string;
meta: ImportFilePreviewPOJO;
}

View File

@@ -33,3 +33,7 @@ export interface IOrganizationBuildEventPayload {
buildDTO: IOrganizationBuildDTO;
systemUser: ISystemUser;
}
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}

View File

@@ -0,0 +1,8 @@
export interface SubscriptionPayload {
lemonSqueezyId?: string;
}
export enum SubscriptionPaymentStatus {
Succeed = 'succeed',
Failed = 'failed',
}

View File

@@ -75,6 +75,7 @@ export * from './Times';
export * from './ProjectProfitabilitySummary';
export * from './TaxRate';
export * from './Plaid';
export * from './Subscription';
export interface I18nService {
__: (input: string) => string;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';
export class Transformer {
public context: any;
@@ -155,19 +156,35 @@ export class Transformer {
this.dateFormat = format;
}
/**
* Format date.
* @param {} date
* @param {string} format -
* @returns {}
*/
protected formatDate(date, format?: string) {
// Use the export date format if the async operation is in exporting,
// otherwise use the given or default format.
const _format = this.context.exportAls.isExport
? EXPORT_DTE_FORMAT
: format || this.dateFormat;
return date ? moment(date).format(_format) : '';
}
/**
*
* @param date
* @returns
* @returns {}
*/
protected formatDate(date) {
return date ? moment(date).format(this.dateFormat) : '';
protected formatDateFromNow(date) {
return date ? moment(date).fromNow(true) : '';
}
/**
*
* @param number
* @returns
* @returns {}
*/
protected formatNumber(number, props?) {
return formatNumber(number, { money: false, ...props });
@@ -177,7 +194,7 @@ export class Transformer {
*
* @param money
* @param options
* @returns
* @returns {}
*/
protected formatMoney(money, options?) {
return formatNumber(money, {

View File

@@ -3,12 +3,17 @@ import { isNull } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
import { Transformer } from './Transformer';
import { ImportAls } from '@/services/Import/ImportALS';
import { ExportAls } from '@/services/Export/ExportAls';
@Service()
export class TransformerInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private exportAls: ExportAls;
/**
* Retrieves the application context of all tenant transformers.
* @param {number} tenantId
@@ -17,10 +22,12 @@ export class TransformerInjectable {
async getApplicationContext(tenantId: number) {
const i18n = this.tenancy.i18n(tenantId);
const organization = await TenantMetadata.query().findOne({ tenantId });
const exportAls = this.exportAls;
return {
organization,
i18n,
exportAls,
};
}

View File

@@ -116,6 +116,8 @@ import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashfl
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
export default () => {
return new EventPublisher();
@@ -246,8 +248,10 @@ export const susbcribers = () => {
DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete,
// Subscription
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp,
TriggerInvalidateCacheOnSubscriptionChange,
// Attachments
AttachmentsOnSaleInvoiceCreated,
@@ -281,6 +285,9 @@ export const susbcribers = () => {
DeleteUncategorizedTransactionsOnAccountDeleting,
// Loops
LoopsEventsSubscriber
LoopsEventsSubscriber,
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
];
};

View File

@@ -2,6 +2,7 @@ import { I18n } from 'i18n';
export default () => new I18n({
locales: ['en', 'ar'],
defaultLocale: 'en',
register: global,
directory: global.__locales_dir,
updateFiles: false,

View File

@@ -95,6 +95,11 @@ export default {
},
},
columns: {
customerType: {
name: 'Customer Type',
type: 'text',
accessor: 'formattedCustomerType',
},
firstName: {
name: 'vendor.field.first_name',
type: 'text',
@@ -135,116 +140,117 @@ export default {
openingBalance: {
name: 'vendor.field.opening_balance',
type: 'number',
printable: false
printable: false,
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
type: 'date',
printable: false
printable: false,
accessor: 'formattedOpeningBalanceAt'
},
currencyCode: {
name: 'vendor.field.currency',
type: 'text',
printable: false
printable: false,
},
status: {
name: 'vendor.field.status',
printable: false
printable: false,
},
note: {
name: 'vendor.field.note',
printable: false
printable: false,
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
type: 'text',
printable: false
printable: false,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
type: 'text',
printable: false
printable: false,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
type: 'text',
printable: false
printable: false,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
type: 'text',
printable: false
printable: false,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
type: 'text',
printable: false
printable: false,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
type: 'text',
printable: false
printable: false,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
type: 'text',
printable: false
printable: false,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
type: 'text',
printable: false
printable: false,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
type: 'text',
printable: false
printable: false,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
type: 'text',
printable: false
printable: false,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
type: 'text',
printable: false
printable: false,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
type: 'text',
printable: false
printable: false,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
type: 'text',
printable: false
printable: false,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
type: 'text',
printable: false
printable: false,
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
printable: false
printable: false,
},
},
fields2: {

View File

@@ -257,25 +257,25 @@ export default {
name: 'item.field.sell_price',
fieldType: 'number',
},
cost_price: {
costPrice: {
name: 'item.field.cost_price',
fieldType: 'number',
},
costAccount: {
costAccountId: {
name: 'item.field.cost_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellAccount: {
sellAccountId: {
name: 'item.field.sell_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
inventoryAccount: {
inventoryAccountId: {
name: 'item.field.inventory_account',
fieldType: 'relation',
relationModel: 'Account',

View File

@@ -1,8 +1,8 @@
/* eslint-disable global-require */
import moment from 'moment';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSettings from './ModelSetting';
import Account from './Account';
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
export default class UncategorizedCashflowTransaction extends mixin(
@@ -166,6 +166,28 @@ export default class UncategorizedCashflowTransaction extends mixin(
pending(query) {
query.where('pending', true);
},
minAmount(query, minAmount) {
query.where('amount', '>=', minAmount);
},
maxAmount(query, maxAmount) {
query.where('amount', '<=', maxAmount);
},
toDate(query, toDate) {
const dateFormat = 'YYYY-MM-DD';
const _toDate = moment(toDate).endOf('day').format(dateFormat);
query.where('date', '<=', _toDate);
},
fromDate(query, fromDate) {
const dateFormat = 'YYYY-MM-DD';
const _fromDate = moment(fromDate).startOf('day').format(dateFormat);
query.where('date', '>=', _fromDate);
},
};
}

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class AccountsExportable extends Exportable {
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
page: 1,
} as IAccountsFilter;

View File

@@ -11,6 +11,10 @@ export class GetAuthMeta {
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
oneClickDemo: {
enable: config.oneClickDemoAccounts.enable,
demoUrl: config.oneClickDemoAccounts.demoUrl,
},
};
}
}

View File

@@ -1,5 +1,6 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import moment from 'moment';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ExcludedBankTransactionsQuery } from './_types';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@@ -39,6 +40,18 @@ export class GetExcludedBankTransactionsService {
if (_query.accountId) {
q.where('account_id', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
})
.pagination(_query.page - 1, _query.pageSize);

View File

@@ -4,6 +4,10 @@ export interface ExcludedBankTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface IBankTransactionUnexcludingEventPayload {

View File

@@ -100,12 +100,14 @@ export class TriggerRecognizedTransactions {
private async triggerRecognizeTransactionsOnImportCommitted({
tenantId,
importId,
meta,
}: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch;
const payload = { tenantId, transactionsCriteria: { batch } };
// Cannot continue if the imported resource is not bank account transactions.
if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
}

View File

@@ -8,7 +8,12 @@ export class CashflowAccountTransformer extends Transformer {
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['formattedAmount'];
return [
'formattedAmount',
'lastFeedsUpdatedAt',
'lastFeedsUpdatedAtFormatted',
'lastFeedsUpdatedFromNow',
];
};
/**
@@ -29,7 +34,7 @@ export class CashflowAccountTransformer extends Transformer {
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: IAccount): string => {
@@ -37,4 +42,22 @@ export class CashflowAccountTransformer extends Transformer {
currencyCode: account.currencyCode,
});
};
/**
* Retrieves the last feeds update at formatted date.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedAtFormatted(account: IAccount): string {
return this.formatDate(account.lastFeedsUpdatedAt);
}
/**
* Retrieves the last feeds updated from now.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedFromNow(account: IAccount): string {
return this.formatDateFromNow(account.lastFeedsUpdatedAt);
}
}

View File

@@ -23,7 +23,7 @@ export class GetRecognizedTransactionsService {
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const _filter = {
const _query = {
page: 1,
pageSize: 20,
...filter,
@@ -41,11 +41,26 @@ export class GetRecognizedTransactionsService {
// Exclude the pending transactions.
q.modify('notPending');
if (_filter.accountId) {
q.where('accountId', _filter.accountId);
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
})
.pagination(_filter.page - 1, _filter.pageSize);
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
tenantId,

View File

@@ -62,6 +62,19 @@ export class GetUncategorizedTransactions {
q.whereNull('matchedBankTransactions.id');
q.orderBy('date', 'DESC');
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
})
.pagination(_query.page - 1, _query.pageSize);

View File

@@ -87,7 +87,7 @@ export class UncategorizedTransactionsImportable extends Importable {
}
/**
* Transformes the import params before storing them.
* Transforms the import params before storing them.
* @param {Record<string, any>} parmas
*/
public transformParams(parmas: Record<string, any>) {

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class CustomersExportable extends Exportable {
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter;
return this.customersApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class VendorsExportable extends Exportable {
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter;
return this.vendorsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ExpensesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
} as IExpensesFilter;
return this.expensesApplication

View File

@@ -0,0 +1,48 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ExportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), () => {
this.markAsExport();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an export operation.
* @param flag Boolean flag to set or unset the export status. Defaults to true.
*/
public markAsExport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isExport', flag);
}
/**
* Checks if the current context is an export operation.
* @returns {boolean} True if the context is an export operation, false otherwise.
*/
public get isExport(): boolean {
return !!this.getStore()?.get('isExport');
}
}

View File

@@ -10,6 +10,7 @@ import { Errors, ExportFormat } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils';
import { ExportPdf } from './ExportPdf';
import { ExportAls } from './ExportAls';
@Service()
export class ExportResourceService {
@@ -22,13 +23,33 @@ export class ExportResourceService {
@Inject()
private exportPdf: ExportPdf;
@Inject()
private exportAls: ExportAls;
/**
*
* @param {number} tenantId
* @param {string} resourceName
* @param {ExportFormat} format
* @returns
*/
public async export(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv
) {
return this.exportAls.run(() =>
this.exportAlsRun(tenantId, resourceName, format)
);
}
/**
* Exports the given resource data through csv, xlsx or pdf.
* @param {number} tenantId - Tenant id.
* @param {string} resourceName - Resource name.
* @param {ExportFormat} format - File format.
*/
public async export(
public async exportAlsRun(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv

View File

@@ -0,0 +1,2 @@
export const EXPORT_SIZE_LIMIT = 9999999;
export const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';

View File

@@ -0,0 +1,105 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ImportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), callback);
}
/**
* Runs a callback function in preview mode within the AsyncLocalStorage context.
* @param callback The function to be executed in preview mode.
* @returns The result of the callback function.
*/
public runPreview<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportPreview();
return callback();
});
}
/**
* Runs a callback function in commit mode within the AsyncLocalStorage context.
* @param {() => T} callback - The function to be executed in commit mode.
* @returns {T} The result of the callback function.
*/
public runCommit<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportCommit();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an import operation.
* @param flag Boolean flag to set or unset the import status. Defaults to true.
*/
public markAsImport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImport', flag);
}
/**
* Marks the current context as an import commit operation.
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
*/
public markAsImportCommit(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportCommit', flag);
}
/**
* Marks the current context as an import preview operation.
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
*/
public markAsImportPreview(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportPreview', flag);
}
/**
* Checks if the current context is an import operation.
* @returns {boolean} True if the context is an import operation, false otherwise.
*/
public get isImport(): boolean {
return !!this.getStore()?.get('isImport');
}
/**
* Checks if the current context is an import commit operation.
* @returns {boolean} True if the context is an import commit operation, false otherwise.
*/
public get isImportCommit(): boolean {
return !!this.getStore()?.get('isImportCommit');
}
/**
* Checks if the current context is an import preview operation.
* @returns {boolean} True if the context is an import preview operation, false otherwise.
*/
public get isImportPreview(): boolean {
return !!this.getStore()?.get('isImportPreview');
}
}

View File

@@ -1,4 +1,3 @@
import XLSX from 'xlsx';
import bluebird from 'bluebird';
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
@@ -27,23 +26,7 @@ export class ImportFileCommon {
@Inject()
private resource: ResourceService;
/**
* Maps the columns of the imported data based on the provided mapping attributes.
* @param {Record<string, any>[]} body - The array of data objects to map.
* @param {ImportMappingAttr[]} map - The mapping attributes.
* @returns {Record<string, any>[]} - The mapped data objects.
*/
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return XLSX.utils.sheet_to_json(worksheet, {});
}
/**
* Imports the given parsed data to the resource storage through registered importable service.
* @param {number} tenantId -

View File

@@ -23,7 +23,7 @@ export class ImportFileMapping {
*/
public async mapping(
tenantId: number,
importId: number,
importId: string,
maps: ImportMappingAttr[]
): Promise<ImportFileMapPOJO> {
const importFile = await Import.query()

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { ImportFilePreviewPOJO } from './interfaces';
import { ImportFileProcess } from './ImportFileProcess';
import { ImportAls } from './ImportALS';
@Service()
export class ImportFilePreview {
@@ -11,15 +12,33 @@ export class ImportFilePreview {
@Inject()
private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId -
* @param {string} importId -
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
this.previewAlsRun(tenantId, importId)
);
}
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
public async previewAlsRun(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
import { chain } from 'lodash';
import { Knex } from 'knex';
import { ServiceError } from '@/exceptions';
import {
ERRORS,
getSheetColumns,
getUnmappedSheetColumns,
readImportFile,
} from './_utils';
import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
import ResourceService from '../Resource/ResourceService';
import UnitOfWork from '../UnitOfWork';
import { ImportFilePreviewPOJO } from './interfaces';
import { Import } from '@/system/models';
import { parseSheetData } from './sheet_utils';
@Service()
export class ImportFileProcess {
@@ -37,7 +33,7 @@ export class ImportFileProcess {
*/
public async import(
tenantId: number,
importId: number,
importId: string,
trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> {
const importFile = await Import.query()
@@ -49,10 +45,10 @@ export class ImportFileProcess {
if (!importFile.isMapped) {
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
}
// Read the imported file.
// Read the imported file and parse the given buffer to get columns
// and sheet data in json format.
const buffer = await readImportFile(importFile.filename);
const sheetData = this.importCommon.parseXlsxSheet(buffer);
const header = getSheetColumns(sheetData);
const [sheetData, sheetColumns] = parseSheetData(buffer);
const resource = importFile.resource;
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
@@ -87,7 +83,7 @@ export class ImportFileProcess {
.flatten()
.value();
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
const totalCount = allData.length;
const createdCount = successedImport.length;

View File

@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
import { ImportAls } from './ImportALS';
@Service()
export class ImportFileProcessCommit {
@@ -14,18 +15,36 @@ export class ImportFileProcessCommit {
@Inject()
private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
@Inject()
private eventPublisher: EventPublisher;
/**
* Commits the imported file under ALS.
* @param {number} tenantId
* @param {string} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public commit(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
this.commitAlsRun(tenantId, importId)
);
}
/**
* Commits the imported file.
* @param {number} tenantId
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async commit(
public async commitAlsRun(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces';
import { Import } from '@/system/models';
import { parseSheetData } from './sheet_utils';
@Service()
export class ImportFileUploadService {
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
const buffer = await readImportFile(filename);
// Parse the buffer file to array data.
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
const [sheetData, sheetColumns] = parseSheetData(buffer);
const coumnsStringified = JSON.stringify(sheetColumns);
// Throws service error if the sheet data is empty.
validateSheetEmpty(sheetData);
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
const coumnsStringified = JSON.stringify(sheetColumns);
try {
// Validates the params Yup schema.
await this.importFileCommon.validateParamsSchema(resource, params);

View File

@@ -55,7 +55,7 @@ export class ImportResourceApplication {
*/
public async mapping(
tenantId: number,
importId: number,
importId: string,
maps: ImportMappingAttr[]
) {
return this.importMappingService.mapping(tenantId, importId, maps);
@@ -67,7 +67,7 @@ export class ImportResourceApplication {
* @param {number} importId - Import id.
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(tenantId: number, importId: number) {
public async preview(tenantId: number, importId: string) {
return this.ImportFilePreviewService.preview(tenantId, importId);
}
@@ -77,7 +77,7 @@ export class ImportResourceApplication {
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async process(tenantId: number, importId: number) {
public async process(tenantId: number, importId: string) {
return this.importProcessCommit.commit(tenantId, importId);
}

View File

@@ -0,0 +1,56 @@
import XLSX from 'xlsx';
import { first } from 'lodash';
/**
* Parses the given sheet buffer to worksheet.
* @param {Buffer} buffer
* @returns {XLSX.WorkSheet}
*/
export function parseFirstSheet(buffer: Buffer): XLSX.WorkSheet {
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return worksheet;
}
/**
* Extracts the given worksheet to columns.
* @param {XLSX.WorkSheet} worksheet
* @returns {Array<string>}
*/
export function extractSheetColumns(worksheet: XLSX.WorkSheet): Array<string> {
// By default, sheet_to_json scans the first row and uses the values as headers.
// With the header: 1 option, the function exports an array of arrays of values.
const sheetCells = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const sheetCols = first(sheetCells) as Array<string>;
return sheetCols.filter((col) => col);
}
/**
* Parses the given worksheet to json values. the keys are columns labels.
* @param {XLSX.WorkSheet} worksheet
* @returns {Array<Record<string, string>>}
*/
export function parseSheetToJson(
worksheet: XLSX.WorkSheet
): Array<Record<string, string>> {
return XLSX.utils.sheet_to_json(worksheet, {});
}
/**
* Parses the given sheet buffer then retrieves the sheet data and columns.
* @param {Buffer} buffer
*/
export function parseSheetData(
buffer: Buffer
): [Array<Record<string, string>>, string[]] {
const worksheet = parseFirstSheet(buffer);
const columns = extractSheetColumns(worksheet);
const data = parseSheetToJson(worksheet);
return [data, columns];
}

View File

@@ -139,24 +139,25 @@ export default class InventoryService {
) {
const agenda = Container.get('agenda');
const commonJobsQuery = {
name: 'compute-item-cost',
lastRunAt: { $exists: false },
'data.tenantId': tenantId,
'data.itemId': itemId,
};
// Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item.
await agenda.cancel({
name: 'compute-item-cost',
nextRunAt: { $ne: null },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $gt: startingDate },
...commonJobsQuery,
'data.startingDate': { $lte: startingDate },
});
// Retrieve any `compute-item-cost` in the queue has lower starting date
// with the same given item.
const dependsJobs = await agenda.jobs({
name: 'compute-item-cost',
nextRunAt: { $ne: null },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $lte: startingDate },
...commonJobsQuery,
'data.startingDate': { $gte: startingDate },
});
// If the depends jobs cleared.
if (dependsJobs.length === 0) {
await agenda.schedule(
config.scheduleComputeItemCost,
@@ -172,6 +173,13 @@ export default class InventoryService {
events.inventory.onComputeItemCostJobScheduled,
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
);
} else {
// Re-schedule the jobs that have higher date from current moment.
await Promise.all(
dependsJobs.map((job) =>
job.schedule(config.scheduleComputeItemCost).save()
)
);
}
}

View File

@@ -43,12 +43,22 @@ export class CreateItem {
itemDTO.sellAccountId
);
}
// Validate the income account id existance if the item is sellable.
this.validators.validateIncomeAccountExistance(
itemDTO.sellable,
itemDTO.sellAccountId
);
if (itemDTO.costAccountId) {
await this.validators.validateItemCostAccountExistance(
tenantId,
itemDTO.costAccountId
);
}
// Validate the cost account id existance if the item is purchasable.
this.validators.validateCostAccountExistance(
itemDTO.purchasable,
itemDTO.costAccountId
);
if (itemDTO.inventoryAccountId) {
await this.validators.validateItemInventoryAccountExistance(
tenantId,

View File

@@ -55,6 +55,11 @@ export class EditItem {
itemDTO.categoryId
);
}
// Validate the income account id existance if the item is sellable.
this.validators.validateIncomeAccountExistance(
itemDTO.sellable,
itemDTO.sellAccountId
);
// Validate the sell account existance on the storage.
if (itemDTO.sellAccountId) {
await this.validators.validateItemSellAccountExistance(
@@ -62,6 +67,11 @@ export class EditItem {
itemDTO.sellAccountId
);
}
// Validate the cost account id existance if the item is purchasable.
this.validators.validateCostAccountExistance(
itemDTO.purchasable,
itemDTO.costAccountId
);
// Validate the cost account existance on the storage.
if (itemDTO.costAccountId) {
await this.validators.validateItemCostAccountExistance(

View File

@@ -85,6 +85,42 @@ export class ItemsValidators {
}
}
/**
* Validates income account existance.
* @param {number|null} sellable - Detarmines if the item sellable.
* @param {number|null} incomeAccountId - Income account id.
* @throws {ServiceError(ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM)}
*/
public validateIncomeAccountExistance(
sellable?: boolean,
incomeAccountId?: number
) {
if (sellable && !incomeAccountId) {
throw new ServiceError(
ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM,
'Income account is require with sellable item.'
);
}
}
/**
* Validates the cost account existance.
* @param {boolean|null} purchasable - Detarmines if the item purchasble.
* @param {number|null} costAccountId - Cost account id.
* @throws {ServiceError(ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM)}
*/
public validateCostAccountExistance(
purchasable: boolean,
costAccountId?: number
) {
if (purchasable && !costAccountId) {
throw new ServiceError(
ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM,
'The cost account is required with purchasable item.'
);
}
}
/**
* Validate item inventory account existance and type.
* @param {number} tenantId

View File

@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntry } from '@/models';
import { entriesAmountDiff } from 'utils';
import { Knex } from 'knex';
const ERRORS = {
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
@@ -58,13 +59,14 @@ export default class ItemsEntriesService {
*/
public async filterInventoryEntries(
tenantId: number,
entries: IItemEntry[]
entries: IItemEntry[],
trx?: Knex.Transaction
): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
// Retrieve entries inventory items.
const inventoryItems = await Item.query()
const inventoryItems = await Item.query(trx)
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IItemsFilter } from '@/interfaces';
import { ItemsApplication } from './ItemsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ItemsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ItemsExportable extends Exportable {
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter;
return this.itemsApplication

View File

@@ -26,6 +26,11 @@ export const ERRORS = {
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM:
'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM:
'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
};
export const DEFAULT_VIEW_COLUMNS = [];

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IManualJournalsFilter } from '@/interfaces';
import { Exportable } from '../Export/Exportable';
import { ManualJournalsApplication } from './ManualJournalsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ManualJournalsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ManualJournalsExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
} as IManualJournalsFilter;
return this.manualJournalsApplication

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import { faker } from '@faker-js/faker';
import uniqid from 'uniqid';
import AuthenticationApplication from '../Authentication/AuthApplication';
import OrganizationService from '../Organization/OrganizationService';
import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SystemUser } from '@/system/models';
import { IAuthSignInPOJO } from '@/interfaces';
import { ICreateOneClickDemoPOJO } from './interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { defaultDemoOrganizationDTO } from './_constants';
@Service()
export class CreateOneClickDemo {
@Inject()
private authApp: AuthenticationApplication;
@Inject()
private organizationService: OrganizationService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
*/
public async createOneClickDemo(): Promise<ICreateOneClickDemoPOJO> {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const email = faker.internet.email();
const password = '123123123';
const demoId = uniqid();
await this.authApp.signUp({ firstName, lastName, email, password });
const signedIn = await this.authApp.signIn(email, password);
const tenantId = signedIn.tenant.id;
const userId = signedIn.user.id;
// Creates a new one-click demo.
await OneClickDemo.query().insert({ key: demoId, tenantId, userId });
const buildJob = await this.organizationService.buildRunJob(
tenantId,
defaultDemoOrganizationDTO,
signedIn.user
);
return { email, demoId, signedIn, buildJob };
}
/**
* Sign-in automicatlly using the demo id one creating an account finish.
* @param {string} oneClickDemoId -
* @returns {Promise<IAuthSignInPOJO>}
*/
async autoSignIn(oneClickDemoId: string): Promise<IAuthSignInPOJO> {
const foundOneclickDemo = await OneClickDemo.query()
.findOne('key', oneClickDemoId)
.throwIfNotFound();
const userId = foundOneclickDemo.userId;
const user = await SystemUser.query().findById(userId);
const email = user.email;
const password = '123123123';
const signedIn = await this.authApp.signIn(email, password);
return signedIn;
}
}

View File

@@ -0,0 +1,17 @@
export class SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
* @returns {Array<>}
*/
get mapping() {
return [];
}
/**
* Retireves the seeder file import params.
* @returns {Record<string, any>}
*/
get importParams() {
return {};
}
}

View File

@@ -0,0 +1,35 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoBankTransactions extends SeedDemoAbstract {
get mapping() {
return [
{ from: 'Date', to: 'date' },
{ from: 'Payee', to: 'payee' },
{ from: 'Description', to: 'description' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Amount', to: 'amount' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `bank-transactions.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'UncategorizedCashflowTransaction';
}
get importParams() {
return {
accountId: 1001,
};
}
}

View File

@@ -0,0 +1,37 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountCustomers extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Customer Type', to: 'customerType' },
{ from: 'First Name', to: 'firstName' },
{ from: 'Last Name', to: 'lastName' },
{ from: 'Display Name', to: 'displayName' },
{ from: 'Email', to: 'email' },
{ from: 'Work Phone Number', to: 'workPhone' },
{ from: 'Personal Phone Number', to: 'personalPhone' },
{ from: 'Company Name', to: 'companyName' },
{ from: 'Website', to: 'website' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `customers.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Customer';
}
}

View File

@@ -0,0 +1,39 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountExpenses extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Payment Account', to: 'paymentAccountId' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Payment Date', to: 'paymentDate' },
{ from: 'Description', to: 'description' },
{ from: 'Publish', to: 'publish' },
{
from: 'Expense Account',
to: 'expenseAccountId',
group: 'categories',
},
{ from: 'Amount', to: 'amount', group: 'categories' },
{ from: 'Line Description', to: 'description', group: 'categories' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `Expenses.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Expense';
}
}

View File

@@ -0,0 +1,45 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountItems extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Item Type', to: 'type' },
{ from: 'Item Name', to: 'name' },
{ from: 'Item Code', to: 'code' },
{ from: 'Sellable', to: 'sellable' },
{ from: 'Purchasable', to: 'purchasable' },
{ from: 'Sell Price', to: 'sellPrice' },
{ from: 'Cost Price', to: 'costPrice' },
{ from: 'Cost Account', to: 'costAccountId' },
{ from: 'Sell Account', to: 'sellAccountId' },
{ from: 'Inventory Account', to: 'inventoryAccountId' },
{ from: 'Sell Description', to: 'sellDescription' },
{
from: 'Purchase Description',
to: 'purchaseDescription',
},
{ from: 'Note', to: 'note' },
{ from: 'Category', to: 'category' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `items.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Item';
}
}

View File

@@ -0,0 +1,36 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountManualJournals extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Date', to: 'date' },
{ from: 'Journal No', to: 'journalNumber' },
{ from: 'Reference No.', to: 'reference' },
{ from: 'Description', to: 'description' },
{ from: 'Publish', to: 'publish' },
{ from: 'Credit', to: 'credit', group: 'entries' },
{ from: 'Debit', to: 'debit', group: 'entries' },
{ from: 'Account', to: 'accountId', group: 'entries' },
{ from: 'Note', to: 'note', group: 'entries' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `manual-journals.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'ManualJournal';
}
}

View File

@@ -0,0 +1,37 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoSaleInvoices extends SeedDemoAbstract {
get mapping() {
return [
{ from: 'Invoice Date', to: 'invoiceDate' },
{ from: 'Due Date', to: 'dueDate' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Invoice No.', to: 'invoiceNo' },
{ from: 'Customer', to: 'customerId' },
{ from: 'Exchange Rate', to: 'exchangeRate' },
{ from: 'Invoice Message', to: 'invoiceMessage' },
{ from: 'Terms & Conditions', to: 'termsConditions' },
{ from: 'Delivered', to: 'delivered' },
{ from: 'Item', to: 'itemId', group: 'entries' },
{ from: 'Rate', to: 'rate', group: 'entries' },
{ from: 'Quantity', to: 'quantity', group: 'entries' },
{ from: 'Description', to: 'description', group: 'entries' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `sale-invoices.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'SaleInvoice';
}
}

View File

@@ -0,0 +1,36 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountVendors extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'First Name', to: 'firstName' },
{ from: 'Last Name', to: 'lastName' },
{ from: 'Display Name', to: 'displayName' },
{ from: 'Email', to: 'email' },
{ from: 'Work Phone Number', to: 'workPhone' },
{ from: 'Personal Phone Number', to: 'personalPhone' },
{ from: 'Company Name', to: 'companyName' },
{ from: 'Website', to: 'website' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `vendors.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Vendor';
}
}

View File

@@ -0,0 +1,25 @@
import { Inject, Service } from 'typedi';
import { CreateOneClickDemo } from './CreateOneClickDemo';
@Service()
export class OneClickDemoApplication {
@Inject()
private createOneClickDemoService: CreateOneClickDemo;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
*/
public createOneClick() {
return this.createOneClickDemoService.createOneClickDemo();
}
/**
* Auto-sign-in to created demo account.
* @param {string} demoId -
* @returns {Promise<IAuthSignInPOJO>}
*/
public autoSignIn(demoId: string) {
return this.createOneClickDemoService.autoSignIn(demoId);
}
}

View File

@@ -0,0 +1,12 @@
export const defaultDemoOrganizationDTO = {
name: 'BIGCAPITAL, INC',
baseCurrency: 'USD',
location: 'US',
language: 'en',
industry: 'Technology',
fiscalYear: 'march',
timezone: 'US/Central',
dateFormat: 'MM/DD/yyyy',
}

View File

@@ -0,0 +1,113 @@
import { Inject } from 'typedi';
import { promises as fs } from 'fs';
import path from 'path';
import uniqid from 'uniqid';
import { isEmpty } from 'lodash';
import events from '@/subscribers/events';
import { PromisePool } from '@supercharge/promise-pool';
import { IOrganizationBuiltEventPayload } from '@/interfaces';
import { SeedDemoAccountItems } from '../DemoSeeders/SeedDemoItems';
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
import { getImportsStoragePath } from '@/services/Import/_utils';
import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SeedDemoAccountCustomers } from '../DemoSeeders/SeedDemoCustomers';
import { SeedDemoAccountVendors } from '../DemoSeeders/SeedDemoVendors';
import { SeedDemoAccountManualJournals } from '../DemoSeeders/SeedDemoManualJournals';
import { SeedDemoAccountExpenses } from '../DemoSeeders/SeedDemoExpenses';
import { SeedDemoBankTransactions } from '../DemoSeeders/SeedDemoBankTransactions';
import { SeedDemoSaleInvoices } from '../DemoSeeders/SeedDemoSaleInvoices';
export class SeedInitialDemoAccountDataOnOrgBuild {
@Inject()
private importApp: ImportResourceApplication;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.organization.built,
this.seedInitialDemoAccountDataOnOrgBuild.bind(this)
);
};
/**
* Demo account seeder.
*/
get seedDemoAccountSeeders() {
return [
SeedDemoAccountItems,
SeedDemoBankTransactions,
SeedDemoAccountCustomers,
SeedDemoAccountVendors,
SeedDemoAccountManualJournals,
SeedDemoSaleInvoices,
SeedDemoAccountExpenses,
];
}
/**
* Initialize the seeder sheet file to the import storage first.
* @param {string} fileName -
* @returns {Promise<void>}
*/
async initiateSeederFile(fileName: string) {
const destFileName = uniqid();
const source = path.join(global.__views_dir, `/demo-sheets`, fileName);
const destination = path.join(getImportsStoragePath(), destFileName);
// Use the fs.promises.copyFile method to copy the file
await fs.copyFile(source, destination);
return destFileName;
}
/**
* Seeds initial demo account data on organization build
* @param {IOrganizationBuildEventPayload}
*/
async seedInitialDemoAccountDataOnOrgBuild({
tenantId,
}: IOrganizationBuiltEventPayload) {
const foundDemo = await OneClickDemo.query().findOne('tenantId', tenantId);
// Can't continue if the found demo is not exists.
// Means that account is not demo account.
if (!foundDemo) {
return null;
}
const results = await PromisePool.for(this.seedDemoAccountSeeders)
.withConcurrency(1)
.process(async (SeedDemoAccountSeeder) => {
const seederInstance = new SeedDemoAccountSeeder();
// Initialize the seeder sheet file before importing.
const importFileName = await this.initiateSeederFile(seederInstance.importFileName);
// Import the given seeder file.
const importedFile = await this.importApp.import(
tenantId,
seederInstance.resource,
importFileName,
seederInstance.importParams
);
// Mapping the columns with resource fields.
await this.importApp.mapping(
tenantId,
importedFile.import.importId,
seederInstance.mapping
);
await this.importApp.preview(tenantId, importedFile.import.importId);
// Commit the imported file.
await this.importApp.process(
tenantId,
importedFile.import.importId
);
});
if (!isEmpty(results.errors)) {
throw results.errors;
}
}
}

View File

@@ -0,0 +1,8 @@
import { IAuthSignInPOJO } from '@/interfaces';
export interface ICreateOneClickDemoPOJO {
email: string;
demoId: string;
signedIn: IAuthSignInPOJO;
buildJob: any;
}

View File

@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
import {
IOrganizationBuildDTO,
IOrganizationBuildEventPayload,
IOrganizationBuiltEventPayload,
IOrganizationUpdateDTO,
ISystemUser,
ITenant,
@@ -17,6 +18,8 @@ import { Tenant } from '@/system/models';
import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
@Service()
export default class OrganizationService {
@@ -62,6 +65,10 @@ export default class OrganizationService {
// Migrated tenant.
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
// Injects the given tenant IoC services.
await initalizeTenantServices(tenantId);
await initializeTenantSettings(tenantId);
// Creates a tenancy object from given tenant model.
const tenancyContext =
this.tenantsManager.getSeedMigrationContext(migratedTenant);
@@ -82,6 +89,11 @@ export default class OrganizationService {
//
await this.flagTenantDBBatch(tenantId);
// Triggers the organization built event.
await this.eventPublisher.emitAsync(events.organization.built, {
tenantId: tenant.id,
} as IOrganizationBuiltEventPayload)
}
/**

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '@/services/Export/Exportable';
import { BillPaymentsApplication } from './BillPaymentsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class BillPaymentExportable extends Exportable {
@@ -14,11 +15,11 @@ export class BillPaymentExportable extends Exportable {
*/
public exportable(tenantId: number, query: any) {
const parsedQuery = {
page: 1,
pageSize: 12,
...query,
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as any;
return this.billPaymentsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IBillsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { BillsApplication } from './BillsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class BillsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class BillsExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
} as IBillsFilter;
return this.billsApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { SaleEstimatesApplication } from './SaleEstimatesApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class SaleEstimatesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleEstimatesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
} as ISalesInvoicesFilter;
return this.saleEstimatesApplication

View File

@@ -44,7 +44,7 @@ export class SaleInvoiceGLEntries {
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode
saleInvoice.currencyCode, {}, trx
);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(

View File

@@ -32,7 +32,8 @@ export class InvoiceInventoryTransactions {
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleInvoice.entries
saleInvoice.entries,
trx
);
const transaction = {
transactionId: saleInvoice.id,

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces';
import { SaleInvoiceApplication } from './SaleInvoicesApplication';
import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class SaleInvoicesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleInvoicesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 120000,
pageSize: EXPORT_SIZE_LIMIT,
} as ISalesInvoicesFilter;
return this.saleInvoicesApplication

View File

@@ -1,3 +1,4 @@
import { Mutex } from 'async-mutex';
import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash';
import moment from 'moment';
@@ -34,17 +35,26 @@ export class SaleInvoicesCost {
inventoryItemsIds: number[],
startingDate: Date
): Promise<void> {
const asyncOpers: Promise<[]>[] = [];
const mutex = new Mutex();
inventoryItemsIds.forEach((inventoryItemId: number) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
inventoryItemId,
startingDate
);
asyncOpers.push(oper);
});
await Promise.all([...asyncOpers]);
const asyncOpers = inventoryItemsIds.map(
async (inventoryItemId: number) => {
// @todo refactor the lock acquire to be distrbuted using Redis
// and run the cost schedule job after running invoice transaction.
const release = await mutex.acquire();
try {
await this.inventoryService.scheduleComputeItemCost(
tenantId,
inventoryItemId,
startingDate
);
} finally {
release();
}
}
);
await Promise.all(asyncOpers);
}
/**
@@ -86,17 +96,22 @@ export class SaleInvoicesCost {
tenantId: number,
inventoryTransactions: IInventoryTransaction[]
) {
const asyncOpers: Promise<[]>[] = [];
const mutex = new Mutex();
const reducedTransactions = this.getMaxDateInventoryTransactions(
inventoryTransactions
);
reducedTransactions.forEach((transaction) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
transaction.itemId,
transaction.date
);
asyncOpers.push(oper);
const asyncOpers = reducedTransactions.map(async (transaction) => {
const release = await mutex.acquire();
try {
await this.inventoryService.scheduleComputeItemCost(
tenantId,
transaction.itemId,
transaction.date
);
} finally {
release();
}
});
await Promise.all([...asyncOpers]);
}

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { PaymentReceivesApplication } from './PaymentReceivedApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class PaymentsReceivedExportable extends Exportable {
@@ -21,6 +22,8 @@ export class PaymentsReceivedExportable extends Exportable {
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IPaymentsReceivedFilter;
return this.paymentReceivedApp

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesReceiptsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { SaleReceiptApplication } from './SaleReceiptApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class SaleReceiptsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleReceiptsExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12,
pageSize: EXPORT_SIZE_LIMIT,
} as ISalesReceiptsFilter;
return this.saleReceiptsApp

View File

@@ -4,8 +4,8 @@ import { configureLemonSqueezy } from './utils';
import { PlanSubscription } from '@/system/models';
import { ServiceError } from '@/exceptions';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ERRORS, IOrganizationSubscriptionCancel } from './types';
import events from '@/subscribers/events';
import { ERRORS, IOrganizationSubscriptionCanceled } from './types';
@Service()
export class LemonCancelSubscription {
@@ -18,12 +18,15 @@ export class LemonCancelSubscription {
* @param {number} subscriptionId
* @returns {Promise<void>}
*/
public async cancelSubscription(tenantId: number) {
public async cancelSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: 'main',
slug: subscriptionSlug,
});
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
@@ -35,13 +38,10 @@ export class LemonCancelSubscription {
if (cancelledSub.error) {
throw new Error(cancelledSub.error.message);
}
await PlanSubscription.query().findById(subscriptionId).patch({
canceledAt: new Date(),
});
// Triggers `onSubscriptionCanceled` event.
// Triggers `onSubscriptionCancelled` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionCanceled,
{ tenantId, subscriptionId } as IOrganizationSubscriptionCanceled
events.subscription.onSubscriptionCancel,
{ tenantId, subscriptionId } as IOrganizationSubscriptionCancel
);
}
}

View File

@@ -18,25 +18,30 @@ export class LemonChangeSubscriptionPlan {
* @param {number} newVariantId - New variant id.
* @returns {Promise<void>}
*/
public async changeSubscriptionPlan(tenantId: number, newVariantId: number) {
public async changeSubscriptionPlan(
tenantId: number,
newVariantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: 'main',
slug: subscriptionSlug,
});
const lemonSubscriptionId = subscription.lemonSubscriptionId;
// Send request to Lemon Squeezy to change the subscription.
const updatedSub = await updateSubscription(lemonSubscriptionId, {
variantId: newVariantId,
invoiceImmediately: true,
});
if (updatedSub.error) {
throw new ServiceError('SOMETHING_WENT_WRONG');
}
// Triggers `onSubscriptionPlanChanged` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionPlanChanged,
events.subscription.onSubscriptionPlanChange,
{
tenantId,
lemonSubscriptionId,

View File

@@ -1,11 +1,11 @@
import { Inject, Service } from 'typedi';
import { updateSubscription } from '@lemonsqueezy/lemonsqueezy.js';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { configureLemonSqueezy } from './utils';
import { PlanSubscription } from '@/system/models';
import { ServiceError } from '@/exceptions';
import { ERRORS, IOrganizationSubscriptionResumed } from './types';
import { updateSubscription } from '@lemonsqueezy/lemonsqueezy.js';
import { ERRORS, IOrganizationSubscriptionResume } from './types';
@Service()
export class LemonResumeSubscription {
@@ -14,15 +14,19 @@ export class LemonResumeSubscription {
/**
* Resumes the main subscription of the given tenant.
* @param {number} tenantId -
* @param {number} tenantId - Tenant id.
* @param {string} subscriptionSlug - Subscription slug by default main subscription.
* @returns {Promise<void>}
*/
public async resumeSubscription(tenantId: number) {
public async resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: 'main',
slug: subscriptionSlug,
});
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
@@ -33,16 +37,12 @@ export class LemonResumeSubscription {
cancelled: false,
});
if (returnedSub.error) {
throw new ServiceError('');
throw new ServiceError(ERRORS.SOMETHING_WENT_WRONG_WITH_LS);
}
// Update the subscription of the organization.
await PlanSubscription.query().findById(subscriptionId).patch({
canceledAt: null,
});
// Triggers `onSubscriptionCanceled` event.
// Triggers `onSubscriptionResume` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionResumed,
{ tenantId, subscriptionId } as IOrganizationSubscriptionResumed
events.subscription.onSubscriptionResume,
{ tenantId, subscriptionId } as IOrganizationSubscriptionResume
);
}
}

View File

@@ -59,11 +59,25 @@ export class LemonSqueezyWebhooks {
const userId = eventBody.meta.custom_data?.user_id;
const tenantId = eventBody.meta.custom_data?.tenant_id;
const subscriptionSlug = 'main';
if (!webhookHasMeta(eventBody)) {
throw new Error("Event body is missing the 'meta' property.");
} else if (webhookHasData(eventBody)) {
if (webhookEvent.startsWith('subscription_payment_')) {
// Marks the main subscription payment as succeed.
if (webhookEvent === 'subscription_payment_success') {
await this.subscriptionService.markSubscriptionPaymentSucceed(
tenantId,
subscriptionSlug
);
// Marks the main subscription payment as failed.
} else if (webhookEvent === 'subscription_payment_failed') {
await this.subscriptionService.markSubscriptionPaymentFailed(
tenantId,
subscriptionSlug
);
}
// Save subscription invoices; eventBody is a SubscriptionInvoice
// Not implemented.
} else if (webhookEvent.startsWith('subscription_')) {
@@ -74,16 +88,39 @@ export class LemonSqueezyWebhooks {
// We assume that the Plan table is up to date.
const plan = await Plan.query().findOne('lemonVariantId', variantId);
// Update the subscription in the database.
const priceId = attributes.first_subscription_item.price_id;
const subscriptionId = eventBody.data.id;
// Throw error early if the given lemon variant id is not associated to any plan.
if (!plan) {
throw new Error(`Plan with variantId ${variantId} not found.`);
} else {
// Update the subscription in the database.
const priceId = attributes.first_subscription_item.price_id;
// Create a new subscription of the tenant.
if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
}
}
// Create a new subscription of the tenant.
if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion(
tenantId,
plan.slug,
subscriptionSlug,
{ lemonSqueezyId: subscriptionId }
);
// Cancel the given subscription of the organization.
} else if (webhookEvent === 'subscription_cancelled') {
await this.subscriptionService.cancelSubscription(
tenantId,
subscriptionSlug
);
} else if (webhookEvent === 'subscription_plan_changed') {
await this.subscriptionService.subscriptionPlanChanged(
tenantId,
plan.slug,
subscriptionSlug
);
} else if (webhookEvent === 'subscription_resumed') {
await this.subscriptionService.resumeSubscription(
tenantId,
subscriptionSlug
);
}
} else if (webhookEvent.startsWith('order_')) {
// Save orders; eventBody is a "Order"

View File

@@ -1,22 +1,29 @@
import { Service } from 'typedi';
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
import { Plan, Tenant } from '@/system/models';
import { Inject, Service } from 'typedi';
import { NotAllowedChangeSubscriptionPlan, ServiceError } from '@/exceptions';
import { Plan, PlanSubscription, Tenant } from '@/system/models';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { SubscriptionPayload, SubscriptionPaymentStatus } from '@/interfaces';
import { ERRORS } from './types';
@Service()
export class Subscription {
@Inject()
private eventPublisher: EventPublisher;
/**
* Give the tenant a new subscription.
* @param {number} tenantId - Tenant id.
* @param {string} planSlug - Plan slug.
* @param {string} invoiceInterval
* @param {number} invoicePeriod
* @param {string} subscriptionSlug
* @param {string} planSlug - Plan slug of the new subscription.
* @param {string} subscriptionSlug - Subscription slug by default takes main subscription
* @param {SubscriptionPayload} payload - Subscription payload.
*/
public async newSubscribtion(
tenantId: number,
planSlug: string,
subscriptionSlug: string = 'main'
) {
subscriptionSlug: string = 'main',
payload?: SubscriptionPayload
): Promise<void> {
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
const plan = await Plan.query().findOne('slug', planSlug).throwIfNotFound();
@@ -45,8 +52,169 @@ export class Subscription {
plan.id,
invoiceInterval,
invoicePeriod,
subscriptionSlug
subscriptionSlug,
payload
);
}
}
/**
* Cancels the given tenant subscription.
* @param {number} tenantId - Tenant id.
* @param {string} subscriptionSlug - Subscription slug.
*/
async cancelSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
): Promise<void> {
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: subscriptionSlug,
});
// Throw error early if the subscription is not exist.
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_NOT_EXIST);
}
// Throw error early if the subscription is already canceled.
if (subscription.canceled()) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_CANCELED);
}
await subscription.$query().patch({ canceledAt: new Date() });
// Triggers `onSubscriptionCancelled` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionCancelled,
{
tenantId,
subscriptionSlug,
}
);
}
/**
* Resumes the given tenant subscription.
* @param {number} tenantId
* @param {string} subscriptionSlug - Subscription slug by deafult main subscription.
* @returns {Promise<void>}
*/
async resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: subscriptionSlug,
});
// Throw error early if the subscription is not exist.
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_NOT_EXIST);
}
// Throw error early if the subscription is not cancelled.
if (!subscription.canceled()) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ALREADY_ACTIVE);
}
await subscription.$query().patch({ canceledAt: null });
// Triggers `onSubscriptionResumed` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionResumed,
{ tenantId, subscriptionSlug }
);
}
/**
* Mark the given subscription payment of the tenant as succeed.
* @param {number} tenantId
* @param {string} newPlanSlug
* @param {string} subscriptionSlug
*/
async subscriptionPlanChanged(
tenantId: number,
newPlanSlug: string,
subscriptionSlug: string = 'main'
): Promise<void> {
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
const newPlan = await Plan.query()
.findOne('slug', newPlanSlug)
.throwIfNotFound();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: subscriptionSlug,
});
if (subscription.planId === newPlan.id) {
throw new ServiceError('');
}
await subscription.$query().patch({ planId: newPlan.id });
// Triggers `onSubscriptionPlanChanged` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionPlanChanged,
{
tenantId,
newPlanSlug,
subscriptionSlug,
}
);
}
/**
* Marks the subscription payment as succeed.
* @param {number} tenantId - Tenant id.
* @param {string} subscriptionSlug - Given subscription slug by default main subscription.
* @returns {Promise<void>}
*/
async markSubscriptionPaymentSucceed(
tenantId: number,
subscriptionSlug: string = 'main'
): Promise<void> {
const subscription = await PlanSubscription.query()
.findOne({ tenantId, slug: subscriptionSlug })
.throwIfNotFound();
await subscription
.$query()
.patch({ paymentStatus: SubscriptionPaymentStatus.Succeed });
// Triggers `onSubscriptionSucceed` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionPaymentSucceed,
{
tenantId,
subscriptionSlug,
}
);
}
/**
* Marks the given subscription payment of the tenant as failed.
* @param {number} tenantId - Tenant id.
* @param {string} subscriptionSlug - Given subscription slug.
* @returns {Prmise<void>}
*/
async markSubscriptionPaymentFailed(
tenantId: number,
subscriptionSlug: string = 'main'
): Promise<void> {
const subscription = await PlanSubscription.query()
.findOne({ tenantId, slug: subscriptionSlug })
.throwIfNotFound();
await subscription
.$query()
.patch({ paymentStatus: SubscriptionPaymentStatus.Failed });
// Triggers `onSubscriptionPaymentFailed` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionPaymentFailed,
{
tenantId,
subscriptionSlug,
}
);
}
}

View File

@@ -20,8 +20,14 @@ export class SubscriptionApplication {
* @param {string} id
* @returns {Promise<void>}
*/
public cancelSubscription(tenantId: number, id: string) {
return this.cancelSubscriptionService.cancelSubscription(tenantId, id);
public cancelSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
return this.cancelSubscriptionService.cancelSubscription(
tenantId,
subscriptionSlug
);
}
/**
@@ -29,8 +35,14 @@ export class SubscriptionApplication {
* @param {number} tenantId
* @returns {Promise<void>}
*/
public resumeSubscription(tenantId: number) {
return this.resumeSubscriptionService.resumeSubscription(tenantId);
public resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
return this.resumeSubscriptionService.resumeSubscription(
tenantId,
subscriptionSlug
);
}
/**

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { IAuthSignedUpEventPayload } from '@/interfaces';
import events from '@/subscribers/events';
import config from '@/config';
import { Subscription } from '../Subscription';
import { Inject, Service } from 'typedi';
@Service()
export class SubscribeFreeOnSignupCommunity {

View File

@@ -0,0 +1,29 @@
import events from '@/subscribers/events';
import Container from 'typedi';
export class TriggerInvalidateCacheOnSubscriptionChange {
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.subscription.onSubscriptionCancelled,
this.triggerInvalidateCache.bind(this)
);
bus.subscribe(
events.subscription.onSubscriptionResumed,
this.triggerInvalidateCache.bind(this)
);
bus.subscribe(
events.subscription.onSubscriptionPlanChanged,
this.triggerInvalidateCache.bind(this)
);
};
private triggerInvalidateCache() {
const io = Container.get('socket');
// Notify the frontend to reflect the new transactions changes.
io.emit('SUBSCRIPTION_CHANGED', { subscriptionSlug: 'main' });
}
}

View File

@@ -1,6 +1,10 @@
export const ERRORS = {
SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT:
'SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT',
SUBSCRIPTION_NOT_EXIST: 'SUBSCRIPTION_NOT_EXIST',
SUBSCRIPTION_ALREADY_CANCELED: 'SUBSCRIPTION_ALREADY_CANCELED',
SUBSCRIPTION_ALREADY_ACTIVE: 'SUBSCRIPTION_ALREADY_ACTIVE',
SOMETHING_WENT_WRONG_WITH_LS: 'SOMETHING_WENT_WRONG_WITH_LS',
};
export interface IOrganizationSubscriptionChanged {
@@ -9,11 +13,20 @@ export interface IOrganizationSubscriptionChanged {
newVariantId: number;
}
export interface IOrganizationSubscriptionCanceled {
export interface IOrganizationSubscriptionCancel {
tenantId: number;
subscriptionId: string;
}
export interface IOrganizationSubscriptionCancelled {
tenantId: number;
subscriptionId: string;
}
export interface IOrganizationSubscriptionResume {
tenantId: number;
subscriptionId: number;
}
export interface IOrganizationSubscriptionResumed {
tenantId: number;
subscriptionId: number;

View File

@@ -10,6 +10,7 @@ import {
} from '@/interfaces';
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost';
import { ImportAls } from '@/services/Import/ImportALS';
@Service()
export default class InventorySubscriber {
@@ -25,6 +26,9 @@ export default class InventorySubscriber {
@Inject('agenda')
private agenda: any;
@Inject()
private importAls: ImportAls;
/**
* Attaches events with handlers.
*/
@@ -86,20 +90,17 @@ export default class InventorySubscriber {
private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({
tenantId,
inventoryTransactions,
trx
trx,
}: IInventoryTransactionsCreatedPayload) => {
const inventoryItemsIds = map(inventoryTransactions, 'itemId');
const inImportPreviewScope = this.importAls.isImportPreview;
runAfterTransaction(trx, async () => {
try {
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
tenantId,
inventoryTransactions
);
} catch (error) {
console.error(error);
}
});
// Avoid running the cost items job if the async process is in import preview.
if (inImportPreviewScope) return;
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
tenantId,
inventoryTransactions
);
};
/**

View File

@@ -35,6 +35,8 @@ export default {
*/
organization: {
build: 'onOrganizationBuild',
built: 'onOrganizationBuilt',
seeded: 'onOrganizationSeeded',
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
@@ -44,10 +46,19 @@ export default {
* Organization subscription.
*/
subscription: {
onSubscriptionCanceled: 'onSubscriptionCanceled',
onSubscriptionCancel: 'onSubscriptionCancel',
onSubscriptionCancelled: 'onSubscriptionCancelled',
onSubscriptionResume: 'onSubscriptionResume',
onSubscriptionResumed: 'onSubscriptionResumed',
onSubscriptionPlanChange: 'onSubscriptionPlanChange',
onSubscriptionPlanChanged: 'onSubscriptionPlanChanged',
onSubscribed: 'onOrganizationSubscribed',
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
},
/**

View File

@@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('oneclick_demos', (table) => {
table.increments('id');
table.string('key');
table.integer('tenant_id').unsigned();
table.integer('user_id').unsigned();
table.timestamps();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('oneclick_demos');
};

View File

@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.table('subscription_plan_subscriptions', (table) => {
table.string('payment_status');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('subscription_plan_subscriptions', (table) => {
table.dropColumn('payment_status');
});
};

View File

@@ -0,0 +1,17 @@
import SystemModel from '@/system/models/SystemModel';
export class OneClickDemo extends SystemModel {
/**
* Table name
*/
static get tableName() {
return 'oneclick_demos';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
}

View File

@@ -3,6 +3,10 @@ import SystemModel from '@/system/models/SystemModel';
import { PlanSubscription } from '..';
export default class Plan extends mixin(SystemModel) {
price: number;
invoiceInternal: number;
invoicePeriod: string;
/**
* Table name.
*/

View File

@@ -198,14 +198,16 @@ export default class Tenant extends BaseModel {
planId,
invoiceInterval,
invoicePeriod,
subscriptionSlug
subscriptionSlug,
payload?,
) {
return Tenant.newSubscription(
this.id,
planId,
invoiceInterval,
invoicePeriod,
subscriptionSlug
subscriptionSlug,
payload
);
}
@@ -217,7 +219,8 @@ export default class Tenant extends BaseModel {
planId: number,
invoiceInterval: 'month' | 'year',
invoicePeriod: number,
subscriptionSlug: string
subscriptionSlug: string,
payload?: { lemonSqueezyId: string }
) {
const period = new SubscriptionPeriod(invoiceInterval, invoicePeriod);
@@ -227,6 +230,7 @@ export default class Tenant extends BaseModel {
planId,
startsAt: period.getStartDate(),
endsAt: period.getEndDate(),
lemonSubscriptionId: payload?.lemonSqueezyId || null,
});
}
}

BIN
packages/server/views/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,23 @@
Payment Date,Reference No.,Payment Account,Description,Currency Code,Exchange Rate,Expense Account,Amount,Line Description,Publish
2024-03-01,REF-1,Petty Cash,Vel et dolorem architecto veniam.,,,Office expenses,9000,Voluptates voluptas corporis vel.,T
2024-03-02,REF-2,Petty Cash,Id est molestias.,,,Office expenses,9000,Eos voluptatem cumque et voluptate reiciendis.,T
2024-03-03,REF-3,Petty Cash,Quam cupiditate at nihil dicta dignissimos non fugit illo.,,,Office expenses,9000,Hic alias rerum sed commodi dolores sint animi perferendis.,T
2024-03-04,REF-4,Petty Cash,Et voluptatem consequatur corrupti beatae sit.,,,Office expenses,9000,Exercitationem impedit praesentium et eaque.,T
2024-03-05,REF-5,Petty Cash,Illo aut ad id et non error et reiciendis optio.,,,Office expenses,9000,Accusantium modi consequuntur eaque consequatur deleniti consequuntur et qui.,T
2024-03-06,REF-6,Petty Cash,Ea consequatur placeat aut et enim.,,,Office expenses,9000,Itaque odio fugiat recusandae.,T
2024-03-07,REF-7,Petty Cash,A expedita consequatur sequi eveniet quos rerum.,,,Office expenses,9000,Quidem doloremque dignissimos totam dolor iure sed necessitatibus optio.,T
2024-03-08,REF-8,Petty Cash,Est libero deleniti animi delectus eligendi necessitatibus expedita fugit.,,,Office expenses,9000,Velit rerum aperiam mollitia ut eius error est quo aut.,T
2024-03-09,REF-9,Petty Cash,Ut dolor tempora quam consequuntur mollitia aut quos consectetur commodi.,,,Office expenses,9000,Culpa architecto ea vero nisi quis voluptas animi.,T
2024-03-10,REF-10,Petty Cash,Nihil hic soluta.,,,Office expenses,9000,Omnis recusandae ducimus vel.,T
2024-03-11,REF-11,Petty Cash,Aspernatur placeat odit asperiores et tempora quam.,,,Office expenses,9000,Sit tempora optio ullam velit beatae architecto et.,T
2024-03-12,REF-12,Petty Cash,Harum soluta sed.,,,Office expenses,9000,Nobis est earum saepe.,T
2024-03-13,REF-13,Petty Cash,Ea quod mollitia non illo dolores voluptatem distinctio.,,,Office expenses,9000,Sit eos dolores autem rerum voluptate quia ipsam.,T
2024-03-14,REF-14,Petty Cash,Et quod distinctio atque.,,,Office expenses,9000,Facilis sed expedita reiciendis.,T
2024-03-15,REF-15,Petty Cash,Omnis delectus tempore.,,,Office expenses,9000,Autem non reprehenderit placeat aut et quo.,T
2024-03-16,REF-16,Petty Cash,Dolores optio qui dolore quia aut explicabo eaque.,,,Office expenses,9000,Odit dolores ut.,T
2024-03-17,REF-17,Petty Cash,Odit quibusdam sunt in a quod error.,,,Office expenses,9000,Quo explicabo quae dolor enim nisi voluptas id et temporibus.,T
2024-03-18,REF-18,Petty Cash,Hic quibusdam officiis voluptatem facilis repellat molestiae non.,,,Office expenses,9000,Quo sit ea et itaque error.,T
2024-03-19,REF-19,Petty Cash,Dolor doloremque quia qui.,,,Office expenses,9000,Ut deleniti laboriosam et.,T
2024-03-20,REF-20,Petty Cash,Ad enim repellat sed et vero aliquid.,,,Office expenses,9000,Error in voluptas non quae quibusdam id excepturi illo neque.,T
2024-03-21,REF-21,Petty Cash,Doloribus ut excepturi.,,,Office expenses,9000,Sint magni et reiciendis harum praesentium vero sit blanditiis.,T
2024-03-22,REF-22,Petty Cash,Id rerum sunt et.,,,Office expenses,9000,Autem magnam eum error ex sunt temporibus exercitationem ullam est.,T
1 Payment Date Reference No. Payment Account Description Currency Code Exchange Rate Expense Account Amount Line Description Publish
2 2024-03-01 REF-1 Petty Cash Vel et dolorem architecto veniam. Office expenses 9000 Voluptates voluptas corporis vel. T
3 2024-03-02 REF-2 Petty Cash Id est molestias. Office expenses 9000 Eos voluptatem cumque et voluptate reiciendis. T
4 2024-03-03 REF-3 Petty Cash Quam cupiditate at nihil dicta dignissimos non fugit illo. Office expenses 9000 Hic alias rerum sed commodi dolores sint animi perferendis. T
5 2024-03-04 REF-4 Petty Cash Et voluptatem consequatur corrupti beatae sit. Office expenses 9000 Exercitationem impedit praesentium et eaque. T
6 2024-03-05 REF-5 Petty Cash Illo aut ad id et non error et reiciendis optio. Office expenses 9000 Accusantium modi consequuntur eaque consequatur deleniti consequuntur et qui. T
7 2024-03-06 REF-6 Petty Cash Ea consequatur placeat aut et enim. Office expenses 9000 Itaque odio fugiat recusandae. T
8 2024-03-07 REF-7 Petty Cash A expedita consequatur sequi eveniet quos rerum. Office expenses 9000 Quidem doloremque dignissimos totam dolor iure sed necessitatibus optio. T
9 2024-03-08 REF-8 Petty Cash Est libero deleniti animi delectus eligendi necessitatibus expedita fugit. Office expenses 9000 Velit rerum aperiam mollitia ut eius error est quo aut. T
10 2024-03-09 REF-9 Petty Cash Ut dolor tempora quam consequuntur mollitia aut quos consectetur commodi. Office expenses 9000 Culpa architecto ea vero nisi quis voluptas animi. T
11 2024-03-10 REF-10 Petty Cash Nihil hic soluta. Office expenses 9000 Omnis recusandae ducimus vel. T
12 2024-03-11 REF-11 Petty Cash Aspernatur placeat odit asperiores et tempora quam. Office expenses 9000 Sit tempora optio ullam velit beatae architecto et. T
13 2024-03-12 REF-12 Petty Cash Harum soluta sed. Office expenses 9000 Nobis est earum saepe. T
14 2024-03-13 REF-13 Petty Cash Ea quod mollitia non illo dolores voluptatem distinctio. Office expenses 9000 Sit eos dolores autem rerum voluptate quia ipsam. T
15 2024-03-14 REF-14 Petty Cash Et quod distinctio atque. Office expenses 9000 Facilis sed expedita reiciendis. T
16 2024-03-15 REF-15 Petty Cash Omnis delectus tempore. Office expenses 9000 Autem non reprehenderit placeat aut et quo. T
17 2024-03-16 REF-16 Petty Cash Dolores optio qui dolore quia aut explicabo eaque. Office expenses 9000 Odit dolores ut. T
18 2024-03-17 REF-17 Petty Cash Odit quibusdam sunt in a quod error. Office expenses 9000 Quo explicabo quae dolor enim nisi voluptas id et temporibus. T
19 2024-03-18 REF-18 Petty Cash Hic quibusdam officiis voluptatem facilis repellat molestiae non. Office expenses 9000 Quo sit ea et itaque error. T
20 2024-03-19 REF-19 Petty Cash Dolor doloremque quia qui. Office expenses 9000 Ut deleniti laboriosam et. T
21 2024-03-20 REF-20 Petty Cash Ad enim repellat sed et vero aliquid. Office expenses 9000 Error in voluptas non quae quibusdam id excepturi illo neque. T
22 2024-03-21 REF-21 Petty Cash Doloribus ut excepturi. Office expenses 9000 Sint magni et reiciendis harum praesentium vero sit blanditiis. T
23 2024-03-22 REF-22 Petty Cash Id rerum sunt et. Office expenses 9000 Autem magnam eum error ex sunt temporibus exercitationem ullam est. T

View File

@@ -0,0 +1,123 @@
Date,Amount,Description,Payee
2024-07-23,-48.25,CREDIT CARD PURCHASE SMART AND FINAL #1 ANAHEIM CA DATE 12/14 15277301931256 9 CARD 5,
2024-08-21,-21.77,PURCHASE AUTHORIZED ON 05/27 ROYAL FARMS 100 MONUMENT AVE A P NATIONAL HARBOR MD 0846248421604 CARD 44,
2024-08-21,-58,72116609 POS PURCHASE EARNIN RE SC 6 721582259,
2024-08-21,-70.61,Dunkin' Donuts,
2024-08-20,-52.34,Uber Eats,
2024-08-19,-15.47,Panera Bread,
2024-08-19,-4.79,5 PURCHASE-SIG DENNY'S 366992 EUGENE OR 6 039488,
2024-08-18,-186,American Airlines,
2024-08-18,-3,3947558 VISA PURCHASE PLAYSTUD 07057808 TAYLOR MI 8840321 748,
2024-08-18,-9.95,Bojangles,
2024-08-15,-134,GrubHub,
2024-08-15,-91.53,671 POS PURCHASE EARNIN RE SC 9671543 349619,
2024-08-15,-4.78,Popeyes,
2024-08-14,496,CASH DEPOSIT,
2024-08-14,-97.67,APPLE 857979823 NEWKIRK #4,
2024-08-07,-2.81,POS PUR 449399193 04/07 TIMESTAMP THE PIZZA PLACE 4205 NEW YORK NY 634330,
2024-08-06,-4.27,QUEENS BAR 35 E GRAND RIVER AVE #9 DETROIT MI CARD 29602991,
2024-08-06,-6.28,Walmart,
2024-08-06,-20.52,60904 PURCHASE-SIG 09/21 TIMESTAMP UPLIFT RE 9123348 562542 SC 63432961 3968766,
2024-08-06,-12.2,UBER 588639690 RIDE 05/12 CA 12095 DEBIT CARD PURCHASE 04/04 TIMESTAMP 8102241,
2024-08-05,-5.58,WITHDRAWAL 02178293119 POS POUR CHOICES,
2024-08-05,-25.98,Planet Fitness,
2024-08-05,-20,CASH APP TRANSFER,
2024-08-04,-38,WITHDRAWAL DEBIT CARD CONSUMER DEBIT PULSZ 252-291-2362 VA DATE 04/12 23978676735612 63546108 CARD 94353,
2024-08-04,-19,CASH APP TRANSFER,
2024-08-04,-3.25,MICROSOFT 979333884,
2024-08-03,-16,TOPGOLF 1010 GREENWOOD BLVD,
2024-08-03,-59.52,EFT DEBIT MICROSOFT XBOX CA 830 08/03 TIMESTAMP,
2024-08-03,-145.09,Venmo,
2024-08-03,-76.02,Albertsons,
2024-08-03,-9.79,STOP & SHOP #25402 WEST CALDWELL MODATE 01/24 31772803806094 17096869 CARD 34321 WITHDRAWAL DEBIT CARD,
2024-08-02,-103.44,Dunkin' Donuts,
2024-08-02,-72,Uber Eats,
2024-08-02,-143.93,Food Lion,
2024-08-02,-128.12,McDonald's,
2024-07-31,-37.47,CHECKCARD 538036 CLEO AI IN MOUNTAIN VIEWCA 172788892 RECURRING,
2024-07-30,-162.69,McDonald's,
2024-07-29,95,Brigit XX/XX #406735377 PMNT RCVD Brigit,
2024-07-29,-45.47,Starbucks,
2024-07-29,-595.11,INTERAC PURCHASE - 67200 TARGET #052,
2024-07-28,-89,TRANSFER PAYPAL ADD TO BALANCE INTERNET PAYMENT,
2024-07-27,-6.04,Krispy Kreme,
2024-07-27,-66.63,McDonald's,
2024-07-26,-133.56,CARD PURCHASE PAYBYPHONE 804665947 11/19,
2024-07-26,-10.02,70252237 AMAZON - LEN N,
2024-07-26,-5,BLAZE PIZZA #31 SIG PUR 9906,
2024-07-25,-30.9,CITGO,
2024-07-25,-69,Tropical Smoothie Cafe,
2024-07-24,-744,Internal Revenue Service,
2024-07-24,-15.61,Walmart,
2024-08-04,-38,WITHDRAWAL DEBIT CARD CONSUMER DEBIT,
2024-08-05,-5.58,WITHDRAWAL POS POUR,
2024-08-06,-6.28,Walmart,
2024-07-24,-15.61,Walmart,
2024-08-03,-145.09,Venmo,
2024-08-20,-52.34,Uber Eats,
2024-08-02,-72,Uber Eats,
2024-07-25,-69,Tropical Smoothie Cafe,
2024-07-28,-89,TRANSFER ADD TO BALANCE INTERNET PAYMENT,
2024-08-03,-76.02,Albertsons,
2024-08-18,-186,American Airlines,
2024-08-15,-91.53,POS PURCHASE EARNIN RE SC 9671543 349619,
2024-07-29,-95,PMNT RCVD Brigit,
2024-08-04,-19,CASH APP TRANSFER,
2024-08-05,-20,CASH APP TRANSFER,
2024-08-14,496,CASH DEPOSIT,
2024-07-31,-37.47,CHECKCARD 538036 CLEO AI IN MOUNTAIN VIEWCA,
2024-07-25,-30.9,Citgo,
2024-08-18,-9.95,Bojangles,
2024-07-24,-744,Internal Revenue Service,
2024-07-27,-6.04,Krispy Kreme,
2024-07-27,-66.63,McDonald's,
2024-07-30,-162.69,McDonald's,
2024-08-02,-128.12,McDonald's,
2024-08-04,-3.25,MICROSOFT,
2024-08-19,-15.47,Panera Bread,
2024-08-05,-25.98,Planet Fitness,
2024-08-15,-4.78,Popeyes,
2024-08-07,-2.81,POS PUR,
2024-08-21,-21.77,PURCHASE AUTHORIZED ON 05/27,
2024-07-29,-45.47,Starbucks,
2024-08-03,-9.79,WITHDRAWAL DEBIT CARD,
2024-08-03,-16,TOPGOLF 1010 GREENWOOD BLVD,
2024-08-04,-38,WITHDRAWAL DEBIT CARD CONSUMER DEBIT,
2024-08-05,-5.58,WITHDRAWAL POS POUR,
2024-08-06,-6.28,Walmart,
2024-07-24,-15.61,Walmart,
2024-08-03,-145.09,Venmo,
2024-08-20,-52.34,Uber Eats,
2024-08-02,-72,Uber Eats,
2024-07-25,-69,Tropical Smoothie Cafe,
2024-07-28,-89,TRANSFER ADD TO BALANCE INTERNET PAYMENT,
2024-08-03,-76.02,Albertsons,
2024-08-18,-186,American Airlines,
2024-08-15,-91.53,POS PURCHASE EARNIN RE SC 9671543 349619,
2024-07-29,-95,PMNT RCVD Brigit,
2024-08-04,-19,CASH APP TRANSFER,
2024-08-05,-20,CASH APP TRANSFER,
2024-08-14,496,CASH DEPOSIT,
2024-07-31,-37.47,CHECKCARD 538036 CLEO AI IN MOUNTAIN VIEWCA,
2024-07-25,-30.9,Citgo,
2024-08-18,-9.95,Bojangles,
2024-07-24,-744,Internal Revenue Service,
2024-07-27,-6.04,Krispy Kreme,
2024-07-27,-66.63,McDonald's,
2024-07-30,-162.69,McDonald's,
2024-08-02,-128.12,McDonald's,
2024-08-04,-3.25,MICROSOFT,
2024-08-19,-15.47,Panera Bread,
2024-08-05,-25.98,Planet Fitness,
2024-08-15,-4.78,Popeyes,
2024-08-07,-2.81,POS PUR,
2024-08-21,-21.77,PURCHASE AUTHORIZED ON 05/27,
2024-07-29,-45.47,Starbucks,
2024-08-03,-9.79,WITHDRAWAL DEBIT CARD,
2024-08-03,-16,TOPGOLF 1010 GREENWOOD BLVD,
2024-08-04,-38,WITHDRAWAL DEBIT CARD CONSUMER DEBIT,
2024-08-05,-5.58,WITHDRAWAL POS POUR,
2024-08-04,-3.25,MICROSOFT,
2024-08-07,-2.81,POS PUR,
2024-08-21,-21.77,PURCHASE AUTHORIZED ON 05/27,
2024-08-03,-9.79,WITHDRAWAL DEBIT CARD,
1 Date Amount Description Payee
2 2024-07-23 -48.25 CREDIT CARD PURCHASE SMART AND FINAL #1 ANAHEIM CA DATE 12/14 15277301931256 9 CARD 5
3 2024-08-21 -21.77 PURCHASE AUTHORIZED ON 05/27 ROYAL FARMS 100 MONUMENT AVE A P NATIONAL HARBOR MD 0846248421604 CARD 44
4 2024-08-21 -58 72116609 POS PURCHASE EARNIN RE SC 6 721582259
5 2024-08-21 -70.61 Dunkin' Donuts
6 2024-08-20 -52.34 Uber Eats
7 2024-08-19 -15.47 Panera Bread
8 2024-08-19 -4.79 5 PURCHASE-SIG DENNY'S 366992 EUGENE OR 6 039488
9 2024-08-18 -186 American Airlines
10 2024-08-18 -3 3947558 VISA PURCHASE PLAYSTUD 07057808 TAYLOR MI 8840321 748
11 2024-08-18 -9.95 Bojangles
12 2024-08-15 -134 GrubHub
13 2024-08-15 -91.53 671 POS PURCHASE EARNIN RE SC 9671543 349619
14 2024-08-15 -4.78 Popeyes
15 2024-08-14 496 CASH DEPOSIT
16 2024-08-14 -97.67 APPLE 857979823 NEWKIRK #4
17 2024-08-07 -2.81 POS PUR 449399193 04/07 TIMESTAMP THE PIZZA PLACE 4205 NEW YORK NY 634330
18 2024-08-06 -4.27 QUEENS BAR 35 E GRAND RIVER AVE #9 DETROIT MI CARD 29602991
19 2024-08-06 -6.28 Walmart
20 2024-08-06 -20.52 60904 PURCHASE-SIG 09/21 TIMESTAMP UPLIFT RE 9123348 562542 SC 63432961 3968766
21 2024-08-06 -12.2 UBER 588639690 RIDE 05/12 CA 12095 DEBIT CARD PURCHASE 04/04 TIMESTAMP 8102241
22 2024-08-05 -5.58 WITHDRAWAL 02178293119 POS POUR CHOICES
23 2024-08-05 -25.98 Planet Fitness
24 2024-08-05 -20 CASH APP TRANSFER
25 2024-08-04 -38 WITHDRAWAL DEBIT CARD CONSUMER DEBIT PULSZ 252-291-2362 VA DATE 04/12 23978676735612 63546108 CARD 94353
26 2024-08-04 -19 CASH APP TRANSFER
27 2024-08-04 -3.25 MICROSOFT 979333884
28 2024-08-03 -16 TOPGOLF 1010 GREENWOOD BLVD
29 2024-08-03 -59.52 EFT DEBIT MICROSOFT XBOX CA 830 08/03 TIMESTAMP
30 2024-08-03 -145.09 Venmo
31 2024-08-03 -76.02 Albertsons
32 2024-08-03 -9.79 STOP & SHOP #25402 WEST CALDWELL MODATE 01/24 31772803806094 17096869 CARD 34321 WITHDRAWAL DEBIT CARD
33 2024-08-02 -103.44 Dunkin' Donuts
34 2024-08-02 -72 Uber Eats
35 2024-08-02 -143.93 Food Lion
36 2024-08-02 -128.12 McDonald's
37 2024-07-31 -37.47 CHECKCARD 538036 CLEO AI IN MOUNTAIN VIEWCA 172788892 RECURRING
38 2024-07-30 -162.69 McDonald's
39 2024-07-29 95 Brigit XX/XX #406735377 PMNT RCVD Brigit
40 2024-07-29 -45.47 Starbucks
41 2024-07-29 -595.11 INTERAC PURCHASE - 67200 TARGET #052
42 2024-07-28 -89 TRANSFER PAYPAL ADD TO BALANCE INTERNET PAYMENT
43 2024-07-27 -6.04 Krispy Kreme
44 2024-07-27 -66.63 McDonald's
45 2024-07-26 -133.56 CARD PURCHASE PAYBYPHONE 804665947 11/19
46 2024-07-26 -10.02 70252237 AMAZON - LEN N
47 2024-07-26 -5 BLAZE PIZZA #31 SIG PUR 9906
48 2024-07-25 -30.9 CITGO
49 2024-07-25 -69 Tropical Smoothie Cafe
50 2024-07-24 -744 Internal Revenue Service
51 2024-07-24 -15.61 Walmart
52 2024-08-04 -38 WITHDRAWAL DEBIT CARD CONSUMER DEBIT
53 2024-08-05 -5.58 WITHDRAWAL POS POUR
54 2024-08-06 -6.28 Walmart
55 2024-07-24 -15.61 Walmart
56 2024-08-03 -145.09 Venmo
57 2024-08-20 -52.34 Uber Eats
58 2024-08-02 -72 Uber Eats
59 2024-07-25 -69 Tropical Smoothie Cafe
60 2024-07-28 -89 TRANSFER ADD TO BALANCE INTERNET PAYMENT
61 2024-08-03 -76.02 Albertsons
62 2024-08-18 -186 American Airlines
63 2024-08-15 -91.53 POS PURCHASE EARNIN RE SC 9671543 349619
64 2024-07-29 -95 PMNT RCVD Brigit
65 2024-08-04 -19 CASH APP TRANSFER
66 2024-08-05 -20 CASH APP TRANSFER
67 2024-08-14 496 CASH DEPOSIT
68 2024-07-31 -37.47 CHECKCARD 538036 CLEO AI IN MOUNTAIN VIEWCA
69 2024-07-25 -30.9 Citgo
70 2024-08-18 -9.95 Bojangles
71 2024-07-24 -744 Internal Revenue Service
72 2024-07-27 -6.04 Krispy Kreme
73 2024-07-27 -66.63 McDonald's
74 2024-07-30 -162.69 McDonald's
75 2024-08-02 -128.12 McDonald's
76 2024-08-04 -3.25 MICROSOFT
77 2024-08-19 -15.47 Panera Bread
78 2024-08-05 -25.98 Planet Fitness
79 2024-08-15 -4.78 Popeyes
80 2024-08-07 -2.81 POS PUR
81 2024-08-21 -21.77 PURCHASE AUTHORIZED ON 05/27
82 2024-07-29 -45.47 Starbucks
83 2024-08-03 -9.79 WITHDRAWAL DEBIT CARD
84 2024-08-03 -16 TOPGOLF 1010 GREENWOOD BLVD
85 2024-08-04 -38 WITHDRAWAL DEBIT CARD CONSUMER DEBIT
86 2024-08-05 -5.58 WITHDRAWAL POS POUR
87 2024-08-06 -6.28 Walmart
88 2024-07-24 -15.61 Walmart
89 2024-08-03 -145.09 Venmo
90 2024-08-20 -52.34 Uber Eats
91 2024-08-02 -72 Uber Eats
92 2024-07-25 -69 Tropical Smoothie Cafe
93 2024-07-28 -89 TRANSFER ADD TO BALANCE INTERNET PAYMENT
94 2024-08-03 -76.02 Albertsons
95 2024-08-18 -186 American Airlines
96 2024-08-15 -91.53 POS PURCHASE EARNIN RE SC 9671543 349619
97 2024-07-29 -95 PMNT RCVD Brigit
98 2024-08-04 -19 CASH APP TRANSFER
99 2024-08-05 -20 CASH APP TRANSFER
100 2024-08-14 496 CASH DEPOSIT
101 2024-07-31 -37.47 CHECKCARD 538036 CLEO AI IN MOUNTAIN VIEWCA
102 2024-07-25 -30.9 Citgo
103 2024-08-18 -9.95 Bojangles
104 2024-07-24 -744 Internal Revenue Service
105 2024-07-27 -6.04 Krispy Kreme
106 2024-07-27 -66.63 McDonald's
107 2024-07-30 -162.69 McDonald's
108 2024-08-02 -128.12 McDonald's
109 2024-08-04 -3.25 MICROSOFT
110 2024-08-19 -15.47 Panera Bread
111 2024-08-05 -25.98 Planet Fitness
112 2024-08-15 -4.78 Popeyes
113 2024-08-07 -2.81 POS PUR
114 2024-08-21 -21.77 PURCHASE AUTHORIZED ON 05/27
115 2024-07-29 -45.47 Starbucks
116 2024-08-03 -9.79 WITHDRAWAL DEBIT CARD
117 2024-08-03 -16 TOPGOLF 1010 GREENWOOD BLVD
118 2024-08-04 -38 WITHDRAWAL DEBIT CARD CONSUMER DEBIT
119 2024-08-05 -5.58 WITHDRAWAL POS POUR
120 2024-08-04 -3.25 MICROSOFT
121 2024-08-07 -2.81 POS PUR
122 2024-08-21 -21.77 PURCHASE AUTHORIZED ON 05/27
123 2024-08-03 -9.79 WITHDRAWAL DEBIT CARD

Some files were not shown because too many files have changed in this diff Show More