Compare commits

...

39 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
2cfa9123b8 fix: Syntax error 2024-09-01 23:32:54 +02:00
Ahmed Bouhuolia
bb9614aafb Merge pull request #646 from bigcapitalhq/events-tracking
feat(server): Events tracking using Posthog
2024-09-01 23:06:38 +02:00
Ahmed Bouhuolia
302564a56e fix: remove the un-used file 2024-09-01 23:02:41 +02:00
Ahmed Bouhuolia
dcfc231d4d feat(server): Events tracking using Posthog 2024-09-01 23:01:25 +02:00
Ahmed Bouhuolia
64080ed678 Merge pull request #644 from bigcapitalhq/set-default-index-entries
fix: Set default index to transaction entries
2024-09-01 15:10:40 +02:00
Ahmed Bouhuolia
263935d91e Merge pull request #643 from bigcapitalhq/import-bugs
fix: Import bugs
2024-09-01 15:02:31 +02:00
Ahmed Bouhuolia
7f5ffb8da1 fix: Add the missing columns to the payment received and made models 2024-09-01 15:00:51 +02:00
Ahmed Bouhuolia
f07d25edbe fix: Set default index to transaction entries 2024-09-01 13:32:47 +02:00
Ahmed Bouhuolia
7c07d6b5ff fix: The unimported functions 2024-09-01 11:16:50 +02:00
Ahmed Bouhuolia
e433f4ad68 fix: Item resource columns labels 2024-09-01 11:07:55 +02:00
Ahmed Bouhuolia
a79b9caff6 Merge pull request #641 from bigcapitalhq/fix-getting-sheet-columns
fix: Getting the sheet columns in import sheet
2024-08-30 17:56:53 +02:00
Ahmed Bouhuolia
2227cead66 Merge pull request #624 from bigcapitalhq/subscription-middleware
fix: Subscription middleware
2024-08-30 17:56:02 +02:00
Ahmed Bouhuolia
410c4ea3e2 fix: Subscription active detarminer 2024-08-30 17:52:53 +02:00
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
ee2d8d3065 fix: subscription middleare 2024-08-25 12:42:42 +02:00
99 changed files with 1824 additions and 456 deletions

View File

@@ -89,3 +89,7 @@ S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=
S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=

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",
@@ -98,6 +99,7 @@
"objection-unique": "^1.2.2",
"plaid": "^10.3.0",
"pluralize": "^8.0.0",
"posthog-node": "^4.2.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
"qim": "0.0.52",

View File

@@ -1,6 +1,8 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
const SupportedMethods = ['POST', 'PUT'];
export default (subscriptionSlug = 'main') =>
async (req: Request, res: Response, next: NextFunction) => {
const { tenant, tenantId } = req;
@@ -19,8 +21,10 @@ export default (subscriptionSlug = 'main') =>
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
}
// Validate in case the subscription is inactive.
else if (subscription.inactive()) {
const isMethodSupported = SupportedMethods.includes(req.method);
const isSubscriptionInactive = subscription.inactive();
if (isMethodSupported && isSubscriptionInactive) {
return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
});

View File

@@ -246,8 +246,19 @@ module.exports = {
apiKey: process.env.LOOPS_API_KEY,
},
oneClickDemoAccounts: parseBoolean(
process.env.ONE_CLICK_DEMO_ACCOUNTS,
false
),
/**
* One-click demo accounts.
*/
oneClickDemoAccounts: {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
/**
* PostHog
*/
posthog: {
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST
}
};

View File

@@ -0,0 +1,43 @@
export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice d';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited';
export const BILL_DELETED = 'Bill deleted';
export const PAYMENT_MADE_CREATED = 'Payment made created';
export const PAYMENT_MADE_EDITED = 'Payment made edited';
export const PAYMENT_MADE_DELETED = 'Payment made deleted';
export const EXPENSE_CREATED = 'Expense created';
export const EXPENSE_EDITED = 'Expense edited';
export const EXPENSE_DELETED = 'Expense deleted';
export const ACCOUNT_CREATED = 'Account created';
export const ACCOUNT_EDITED = 'Account Edited';
export const ACCOUNT_DELETED = 'Account deleted';
export const ITEM_EVENT_CREATED = 'Item created';
export const ITEM_EVENT_EDITED = 'Item edited';
export const ITEM_EVENT_DELETED = 'Item deleted';
export const AUTH_SIGNED_UP = 'Auth Signed-up';
export const AUTH_RESET_PASSWORD = 'Auth reset password';
export const ACCOUNT_GROUP = 'Account';
export const ITEM_GROUP = 'Item';
export const AUTH_GROUP = 'Auth';
export const SALE_GROUP = 'Sale';
export const PAYMENT_GROUP = 'Payment';
export const BILL_GROUP = 'Bill';
export const EXPENSE_GROUP = 'Expense';

View File

@@ -76,6 +76,10 @@ export interface IAuthSendedResetPassword {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
oneClickDemo: {
enable: boolean;
demoUrl: string;
};
}
export interface IAuthSignUpVerifingEventPayload {

View File

@@ -1,10 +1,10 @@
import { Knex } from 'knex';
import { IDynamicListFilter, IItemEntry, IVendorCredit } from '@/interfaces';
import { IDynamicListFilter, IItemEntry } from '@/interfaces';
import { ILedgerEntry } from './Ledger';
import { AttachmentLinkDTO } from './Attachments';
export interface ICreditNoteEntryNewDTO {
index: number;
index?: number;
itemId: number;
rate: number;
quantity: number;
@@ -22,7 +22,7 @@ export interface ICreditNoteNewDTO {
entries: ICreditNoteEntryNewDTO[];
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[]
attachments?: AttachmentLinkDTO[];
}
export interface ICreditNoteEditDTO {
@@ -35,7 +35,7 @@ export interface ICreditNoteEditDTO {
entries: ICreditNoteEntryNewDTO[];
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[]
attachments?: AttachmentLinkDTO[];
}
export interface ICreditNoteEntry extends IItemEntry {}
@@ -61,7 +61,7 @@ export interface ICreditNote {
localAmount?: number;
branchId?: number;
warehouseId: number;
createdAt?: Date,
createdAt?: Date;
}
export enum CreditNoteAction {

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

@@ -48,6 +48,7 @@ export interface IItemEntry {
export interface IItemEntryDTO {
id?: number;
index?: number;
itemId: number;
landedCost?: boolean;
warehouseId?: number;

View File

@@ -66,7 +66,7 @@ export interface IPaymentReceivedEntry {
export interface IPaymentReceivedEntryDTO {
id?: number;
index: number;
index?: number;
paymentReceiveId?: number;
invoiceId: number;
paymentAmount: number;
@@ -74,6 +74,7 @@ export interface IPaymentReceivedEntryDTO {
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
filterQuery?: (trx: Knex.Transaction) => void;
}
export interface IPaymentReceivePageEntry {

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;
@@ -156,22 +157,34 @@ export class Transformer {
}
/**
*
* @param date
* @returns
* Format date.
* @param {} date
* @param {string} format -
* @returns {}
*/
protected formatDate(date) {
return date ? moment(date).format(this.dateFormat) : '';
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) : '';
}
protected formatDateFromNow(date){
/**
*
* @param date
* @returns {}
*/
protected formatDateFromNow(date) {
return date ? moment(date).fromNow(true) : '';
}
/**
*
* @param number
* @returns
* @returns {}
*/
protected formatNumber(number, props?) {
return formatNumber(number, { money: false, ...props });
@@ -181,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

@@ -118,6 +118,7 @@ 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';
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
export default () => {
return new EventPublisher();
@@ -289,5 +290,7 @@ export const susbcribers = () => {
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
...EventsTrackerListeners
];
};

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'bill_date',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
@@ -77,7 +79,7 @@ export default {
paymentDate: {
name: 'bill_payment.field.payment_date',
type: 'date',
accessor: 'formattedPaymentDate'
accessor: 'formattedPaymentDate',
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
@@ -111,6 +113,34 @@ export default {
name: 'bill_payment.field.reference',
type: 'text',
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Bill date',
accessor: 'bill.formattedBillDate',
},
billNo: {
name: 'Bill No.',
accessor: 'bill.billNo',
},
billRefNo: {
name: 'Bill Reference No.',
accessor: 'bill.referenceNo',
},
billAmount: {
name: 'Bill Amount',
accessor: 'bill.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
},
fields2: {
vendorId: {

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

@@ -41,17 +41,17 @@ export default {
fieldType: 'boolean',
},
sell_price: {
name: 'item.field.cost_price',
name: 'item.field.sell_price',
column: 'sell_price',
fieldType: 'number',
},
cost_price: {
name: 'item.field.cost_account',
name: 'item.field.cost_price',
column: 'cost_price',
fieldType: 'number',
},
cost_account: {
name: 'item.field.sell_account',
name: 'item.field.cost_account',
column: 'cost_account_id',
fieldType: 'relation',
@@ -62,7 +62,7 @@ export default {
relationEntityKey: 'slug',
},
sell_account: {
name: 'item.field.sell_description',
name: 'item.field.sell_account',
column: 'sell_account_id',
fieldType: 'relation',
@@ -155,24 +155,24 @@ export default {
printable: false,
},
sellPrice: {
name: 'item.field.cost_price',
name: 'item.field.sell_price',
type: 'number',
exportable: true,
},
costPrice: {
name: 'item.field.cost_account',
name: 'item.field.cost_price',
type: 'number',
exportable: true,
},
costAccount: {
name: 'item.field.sell_account',
name: 'item.field.cost_account',
type: 'text',
accessor: 'costAccount.name',
exportable: true,
printable: false,
},
sellAccount: {
name: 'item.field.sell_description',
name: 'item.field.sell_account',
type: 'text',
accessor: 'sellAccount.name',
exportable: true,

View File

@@ -1,6 +1,9 @@
export default {
importable: true,
exportable: true,
exportFlattenOn: 'entries',
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo',
@@ -72,7 +75,7 @@ export default {
amount: {
name: 'payment_receive.field.amount',
type: 'number',
accessor: 'formattedAmount'
accessor: 'formattedAmount',
},
referenceNo: {
name: 'payment_receive.field.reference_no',
@@ -92,6 +95,34 @@ export default {
type: 'text',
printable: false,
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Invoice date',
accessor: 'invoice.invoiceDateFormatted',
},
invoiceNo: {
name: 'Invoice No.',
accessor: 'invoice.invoiceNo',
},
invoiceRefNo: {
name: 'Invoice Reference No.',
accessor: 'invoice.referenceNo',
},
invoiceAmount: {
name: 'Invoice Amount',
accessor: 'invoice.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
created_at: {
name: 'payment_receive.field.created_at',
type: 'date',

View File

@@ -13,7 +13,7 @@ export default {
importAggregateBy: 'estimateNumber',
print: {
pageTitle: 'Sale Estimates'
pageTitle: 'Sale Estimates',
},
fields: {
@@ -142,6 +142,7 @@ export default {
delivered: {
name: 'Delivered',
type: 'boolean',
accessor: 'isDelivered',
exportable: true,
printable: false,
},

View File

@@ -155,6 +155,7 @@ export default {
name: 'invoice.field.delivered',
type: 'boolean',
printable: false,
accessor: 'isDelivered',
},
entries: {
name: 'Entries',

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

@@ -100,7 +100,6 @@ export class TriggerRecognizedTransactions {
private async triggerRecognizeTransactionsOnImportCommitted({
tenantId,
importId,
meta,
}: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch;

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

@@ -5,11 +5,17 @@ import * as R from 'ramda';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { ICreditNote, ICreditNoteEditDTO, ICreditNoteNewDTO } from '@/interfaces';
import {
ICreditNote,
ICreditNoteEditDTO,
ICreditNoteEntryNewDTO,
ICreditNoteNewDTO,
} from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
@Service()
export default class BaseCreditNotes {
@@ -43,10 +49,17 @@ export default class BaseCreditNotes {
const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries
);
const entries = creditNoteDTO.entries.map((entry) => ({
...entry,
referenceType: 'CreditNote',
}));
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex,
// Associate the reference type to credit note entries.
R.map((entry: ICreditNoteEntryNewDTO) => ({
...entry,
referenceType: 'CreditNote',
}))
)(creditNoteDTO.entries);
// Retreive the next credit note number.
const autoNextNumber = this.getNextCreditNumber(tenantId);

View File

@@ -0,0 +1,24 @@
import { PostHog } from 'posthog-node';
import { Service } from 'typedi';
import { EventMessage } from 'posthog-node/src/types';
import config from '@/config';
@Service()
export class PosthogService {
public posthog: PostHog;
constructor() {
if (config.posthog.apiKey && config.posthog.host) {
this.posthog = new PostHog(config.posthog.apiKey, {
host: config.posthog.host,
});
}
}
public trackEvent(event: EventMessage) {
// Cannot continue if the Posthog not configured.
if (!this.posthog) return;
this.posthog.capture(event);
}
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IAccountEventCreatedPayload,
IAccountEventEditedPayload,
IAccountEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
ACCOUNT_CREATED,
ACCOUNT_EDITED,
ACCOUNT_DELETED,
} from '@/constants/event-tracker';
@Service()
export class AccountEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.accounts.onCreated,
this.handleTrackAccountCreatedEvent
);
bus.subscribe(events.accounts.onEdited, this.handleTrackEditedAccountEvent);
bus.subscribe(
events.accounts.onDeleted,
this.handleTrackDeletedAccountEvent
);
}
private handleTrackAccountCreatedEvent = ({
tenantId,
}: IAccountEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_CREATED,
properties: {},
});
};
private handleTrackEditedAccountEvent = ({
tenantId,
}: IAccountEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_EDITED,
properties: {},
});
};
private handleTrackDeletedAccountEvent = ({
tenantId,
}: IAccountEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { IAuthSignedUpEventPayload } from '@/interfaces';
import { PosthogService } from '../PostHog';
import { AUTH_SIGNED_UP } from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class AuthenticationEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.auth.signUp, this.handleTrackSignUpEvent);
}
private handleTrackSignUpEvent = ({
signupDTO,
user,
tenant,
}: IAuthSignedUpEventPayload) => {
this.posthog.trackEvent({
distinctId: user.email,
event: AUTH_SIGNED_UP,
properties: {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
},
});
};
}

View File

@@ -0,0 +1,59 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEventEditedPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BILL_CREATED,
BILL_EDITED,
BILL_DELETED,
} from '@/constants/event-tracker';
@Service()
export class BillEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.bill.onCreated, this.handleTrackBillCreatedEvent);
bus.subscribe(events.bill.onEdited, this.handleTrackEditedBillEvent);
bus.subscribe(events.bill.onDeleted, this.handleTrackDeletedBillEvent);
}
private handleTrackBillCreatedEvent = ({
tenantId,
}: IBillPaymentEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_CREATED,
properties: {},
});
};
private handleTrackEditedBillEvent = ({
tenantId,
}: IBillPaymentEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_EDITED,
properties: {},
});
};
private handleTrackDeletedBillEvent = ({
tenantId,
}: IBillPaymentEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IExpenseCreatedPayload,
IExpenseEventEditPayload,
IExpenseEventDeletePayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
EXPENSE_CREATED,
EXPENSE_EDITED,
EXPENSE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ExpenseEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.expenses.onCreated,
this.handleTrackExpenseCreatedEvent
);
bus.subscribe(events.expenses.onEdited, this.handleTrackEditedExpenseEvent);
bus.subscribe(
events.expenses.onDeleted,
this.handleTrackDeletedExpenseEvent
);
}
private handleTrackExpenseCreatedEvent = ({
tenantId,
}: IExpenseCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_CREATED,
properties: {},
});
};
private handleTrackEditedExpenseEvent = ({
tenantId,
}: IExpenseEventEditPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_EDITED,
properties: {},
});
};
private handleTrackDeletedExpenseEvent = ({
tenantId,
}: IExpenseEventDeletePayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,59 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IItemEventCreatedPayload,
IItemEventEditedPayload,
IItemEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
ITEM_EVENT_CREATED,
ITEM_EVENT_EDITED,
ITEM_EVENT_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ItemEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
}
private handleTrackItemCreatedEvent = ({
tenantId,
}: IItemEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_CREATED,
properties: {},
});
};
private handleTrackEditedItemEvent = ({
tenantId,
}: IItemEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_EDITED,
properties: {},
});
};
private handleTrackDeletedItemEvent = ({
tenantId,
}: IItemEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEditingPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_MADE_CREATED,
PAYMENT_MADE_EDITED,
PAYMENT_MADE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentMadeEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.billPayment.onCreated,
this.handleTrackPaymentMadeCreatedEvent
);
bus.subscribe(
events.billPayment.onEdited,
this.handleTrackEditedPaymentMadeEvent
);
bus.subscribe(
events.billPayment.onDeleted,
this.handleTrackDeletedPaymentMadeEvent
);
}
private handleTrackPaymentMadeCreatedEvent = ({
tenantId,
}: IBillPaymentEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_CREATED,
properties: {},
});
};
private handleTrackEditedPaymentMadeEvent = ({
tenantId,
}: IBillPaymentEditingPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_EDITED,
properties: {},
});
};
private handleTrackDeletedPaymentMadeEvent = ({
tenantId,
}: IBillPaymentEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IPaymentReceivedCreatedPayload,
IPaymentReceivedEditedPayload,
IPaymentReceivedDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_RECEIVED_CREATED,
PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentReceivedEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentReceive.onCreated,
this.handleTrackPaymentReceivedCreatedEvent
);
bus.subscribe(
events.paymentReceive.onEdited,
this.handleTrackEditedPaymentReceivedEvent
);
bus.subscribe(
events.paymentReceive.onDeleted,
this.handleTrackDeletedPaymentReceivedEvent
);
}
private handleTrackPaymentReceivedCreatedEvent = ({
tenantId,
}: IPaymentReceivedCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_CREATED,
properties: {},
});
};
private handleTrackEditedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_EDITED,
properties: {},
});
};
private handleTrackDeletedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleEstimateCreatedPayload,
ISaleEstimateEditedPayload,
ISaleEstimateDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
SALE_ESTIMATE_CREATED,
SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class SaleEstimateEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleEstimate.onCreated,
this.handleTrackEstimateCreatedEvent
);
bus.subscribe(
events.saleEstimate.onEdited,
this.handleTrackEditedEstimateEvent
);
bus.subscribe(
events.saleEstimate.onDeleted,
this.handleTrackDeletedEstimateEvent
);
}
private handleTrackEstimateCreatedEvent = ({
tenantId,
}: ISaleEstimateCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_CREATED,
properties: {},
});
};
private handleTrackEditedEstimateEvent = ({
tenantId,
}: ISaleEstimateEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_EDITED,
properties: {},
});
};
private handleTrackDeletedEstimateEvent = ({
tenantId,
}: ISaleEstimateDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED,
} from '@/constants/event-tracker';
@Service()
export class SaleInvoiceEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleTrackInvoiceCreatedEvent
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleTrackEditedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onDeleted,
this.handleTrackDeletedInvoiceEvent
);
}
private handleTrackInvoiceCreatedEvent = ({
tenantId,
}: ISaleInvoiceCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_CREATED,
properties: {},
});
};
private handleTrackEditedInvoiceEvent = ({
tenantId,
}: ISaleInvoiceEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_EDITED,
properties: {},
});
};
private handleTrackDeletedInvoiceEvent = ({
tenantId,
}: ISaleInvoiceEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,21 @@
import { SaleInvoiceEventsTracker } from './SaleInvoicesEventsTracker';
import { SaleEstimateEventsTracker } from './SaleEstimateEventsTracker';
import { PaymentMadeEventsTracker } from './PaymentMadeEventsTracker';
import { PaymentReceivedEventsTracker } from './PaymentReceivedEventsTracker';
import { BillEventsTracker } from './BillEventsTracker';
import { ExpenseEventsTracker } from './ExpenseEventsTracker';
import { AccountEventsTracker } from './AccountEventsTracker';
import { AuthenticationEventsTracker } from './AuthenticationEventsTracker';
import { ItemEventsTracker } from './ItemEventsTracker';
export const EventsTrackerListeners = [
SaleInvoiceEventsTracker,
SaleEstimateEventsTracker,
PaymentMadeEventsTracker,
PaymentReceivedEventsTracker,
BillEventsTracker,
AccountEventsTracker,
ExpenseEventsTracker,
AuthenticationEventsTracker,
ItemEventsTracker
];

View File

@@ -11,6 +11,7 @@ import {
} from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TenantMetadata } from '@/system/models';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service()
export class ExpenseDTOTransformer {
@@ -40,8 +41,8 @@ export class ExpenseDTOTransformer {
/**
* Mapping expense DTO to model.
* @param {IExpenseDTO} expenseDTO
* @param {ISystemUser} authorizedUser
* @param {IExpenseDTO} expenseDTO
* @param {ISystemUser} authorizedUser
* @return {IExpense}
*/
private expenseDTOToModel(
@@ -52,9 +53,14 @@ export class ExpenseDTOTransformer {
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
const categories = R.compose(
// Associate the default index to categories lines.
assocItemEntriesDefaultIndex
)(expenseDTO.categories || []);
const initialDTO = {
categories: [],
...omit(expenseDTO, ['publish', 'attachments']),
categories,
totalAmount,
landedCostAmount,
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),

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

@@ -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,13 +12,31 @@ 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: string
): Promise<ImportFilePreviewPOJO> {

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 {
@@ -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,16 +15,34 @@ 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: string
): Promise<ImportFilePreviewPOJO> {

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

@@ -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

@@ -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

@@ -0,0 +1,16 @@
import { IItemEntry } from '@/interfaces';
import { isNull, isUndefined } from 'lodash';
export function assocItemEntriesDefaultIndex<T>(
entries: Array<T & { index?: number }>
): Array<T & { index: number }> {
return entries.map((entry, index) => {
return {
index:
isUndefined(entry.index) || isNull(entry.index)
? index + 1
: entry.index,
...entry,
};
});
}

View File

@@ -18,6 +18,8 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandManualJournalValidators } from './CommandManualJournalValidators';
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
@Service()
export class CreateManualJournalService {
@Inject()
@@ -58,16 +60,22 @@ export class CreateManualJournalService {
// The manual or auto-increment journal number.
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex
)(manualJournalDTO.entries);
const initialDTO = {
...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish
? { publishedAt: moment().toMySqlDateTime() }
: {}),
amount,
date,
currencyCode: manualJournalDTO.currencyCode || baseCurrency,
exchangeRate: manualJournalDTO.exchangeRate || 1,
date,
journalNumber,
entries,
userId: authorizedUser.id,
};
return R.compose(

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

@@ -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 {
@@ -13,12 +14,16 @@ export class BillPaymentExportable extends Exportable {
* @returns
*/
public exportable(tenantId: number, query: any) {
const filterQuery = (builder) => {
builder.withGraphFetched('entries.bill');
};
const parsedQuery = {
page: 1,
pageSize: 12,
...query,
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
filterQuery
} as any;
return this.billPaymentsApplication

View File

@@ -3,8 +3,8 @@ import * as R from 'ramda';
import { omit, sumBy } from 'lodash';
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { formatDateFields } from '@/utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class CommandBillPaymentDTOTransformer {
@@ -27,6 +27,12 @@ export class CommandBillPaymentDTOTransformer {
const amount =
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
// Associate the default index to each item entry.
const entries = R.compose(
// Associate the default index to payment entries.
assocItemEntriesDefaultIndex
)(billPaymentDTO.entries);
const initialDTO = {
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
'paymentDate',
@@ -34,7 +40,7 @@ export class CommandBillPaymentDTOTransformer {
amount,
currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries,
entries,
};
return R.compose(
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)

View File

@@ -52,6 +52,7 @@ export class GetBillPayments {
builder.withGraphFetched('paymentAccount');
dynamicList.buildQuery()(builder);
filter?.filterQuery && filter?.filterQuery(builder);
})
.pagination(filter.page - 1, filter.pageSize);

View File

@@ -3,7 +3,7 @@ import moment from 'moment';
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import composeAsync from 'async/compose';
import { formatDateFields } from 'utils';
import { assocDepthLevelToObjectTree, formatDateFields } from 'utils';
import {
IBillDTO,
IBill,
@@ -15,6 +15,7 @@ import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service()
export class BillDTOTransformer {
@@ -54,9 +55,9 @@ export class BillDTOTransformer {
/**
* Converts create bill DTO to model.
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {IBill} oldBill
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {IBill} oldBill
* @returns {IBill}
*/
public async billDTOToModel(
@@ -92,7 +93,9 @@ export class BillDTOTransformer {
const entries = R.compose(
// Remove tax code from entries.
R.map(R.omit(['taxCode']))
R.map(R.omit(['taxCode'])),
// Associate the default index to each item entry line.
assocItemEntriesDefaultIndex
)(asyncEntries);
const initialDTO = {

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

@@ -9,11 +9,13 @@ import {
IVendorCredit,
IVendorCreditCreateDTO,
IVendorCreditEditDTO,
IVendorCreditEntryDTO,
} from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service()
export default class BaseVendorCredit {
@@ -50,10 +52,18 @@ export default class BaseVendorCredit {
const amount = this.itemsEntriesService.getTotalItemsEntries(
vendorCreditDTO.entries
);
const entries = vendorCreditDTO.entries.map((entry) => ({
...entry,
referenceType: 'VendorCredit',
}));
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex,
// Associate the reference type to item entries.
R.map((entry: IVendorCreditEntryDTO) => ({
referenceType: 'VendorCredit',
...entry,
}))
)(vendorCreditDTO.entries);
// Retreive the next vendor credit number.
const autoNextNumber = this.getNextCreditNumber(tenantId);

View File

@@ -9,6 +9,7 @@ import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrat
import { formatDateFields } from '@/utils';
import moment from 'moment';
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service()
export class SaleEstimateDTOTransformer {
@@ -56,6 +57,14 @@ export class SaleEstimateDTOTransformer {
// Validate the sale estimate number require.
this.validators.validateEstimateNoRequire(estimateNumber);
const entries = R.compose(
// Associate the reference type to item entries.
R.map((entry) => R.assoc('reference_type', 'SaleEstimate', entry)),
// Associate default index to item entries.
assocItemEntriesDefaultIndex
)(estimateDTO.entries);
const initialDTO = {
amount,
...formatDateFields(
@@ -65,10 +74,7 @@ export class SaleEstimateDTOTransformer {
currencyCode: paymentCustomer.currencyCode,
exchangeRate: estimateDTO.exchangeRate || 1,
...(estimateNumber ? { estimateNumber } : {}),
entries: estimateDTO.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...entry,
})),
entries,
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && {

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

@@ -17,6 +17,7 @@ import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
import { formatDateFields } from 'utils';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { ItemEntry } from '@/models';
@Service()
@@ -81,7 +82,10 @@ export class CommandSaleInvoiceDTOTransformer {
const entries = R.compose(
// Remove tax code from entries.
R.map(R.omit(['taxCode']))
R.map(R.omit(['taxCode'])),
// Associate the default index for each item entry lin.
assocItemEntriesDefaultIndex
)(asyncEntries);
const initialDTO = {

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

@@ -50,7 +50,9 @@ export class GetPaymentReceives {
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicList.buildQuery()(builder);
filterDTO?.filterQuery && filterDTO.filterQuery(builder);
})
.pagination(filter.page - 1, filter.pageSize);

View File

@@ -11,6 +11,7 @@ import { PaymentReceivedValidators } from './PaymentReceivedValidators';
import { PaymentReceivedIncrement } from './PaymentReceivedIncrement';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service()
export class PaymentReceiveDTOTransformer {
@@ -52,6 +53,11 @@ export class PaymentReceiveDTOTransformer {
this.validators.validatePaymentNoRequire(paymentReceiveNo);
const entries = R.compose(
// Associate the default index to each item entry line.
assocItemEntriesDefaultIndex
)(paymentReceiveDTO.entries);
const initialDTO = {
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
'paymentDate',
@@ -60,9 +66,7 @@ export class PaymentReceiveDTOTransformer {
currencyCode: customer.currencyCode,
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries: paymentReceiveDTO.entries.map((entry) => ({
...entry,
})),
entries,
};
return R.compose(
this.branchDTOTransform.transformDTO<IPaymentReceived>(tenantId)

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 {
@@ -15,12 +16,19 @@ export class PaymentsReceivedExportable extends Exportable {
* @returns
*/
public exportable(tenantId: number, query: IPaymentsReceivedFilter) {
const filterQuery = (builder) => {
builder.withGraphFetched('entries.invoice');
};
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
filterQuery,
} as IPaymentsReceivedFilter;
return this.paymentReceivedApp

View File

@@ -11,6 +11,7 @@ import { ICustomer, ISaleReceipt, ISaleReceiptDTO } from '@/interfaces';
import { formatDateFields } from '@/utils';
import { SaleReceiptIncrement } from './SaleReceiptIncrement';
import { ItemEntry } from '@/models';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service()
export class SaleReceiptDTOTransformer {
@@ -61,11 +62,16 @@ export class SaleReceiptDTOTransformer {
...entry,
}));
const entries = await composeAsync(
const asyncEntries = await composeAsync(
// Sets default cost and sell account to receipt items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);
const entries = R.compose(
// Associate the default index for each item entry.
assocItemEntriesDefaultIndex
)(asyncEntries);
const initialDTO = {
amount,
...formatDateFields(

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

@@ -18,7 +18,10 @@ export class LemonResumeSubscription {
* @param {string} subscriptionSlug - Subscription slug by default main subscription.
* @returns {Promise<void>}
*/
public async resumeSubscription(tenantId: number, subscriptionSlug: string = 'main') {
public async resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
@@ -34,7 +37,7 @@ export class LemonResumeSubscription {
cancelled: false,
});
if (returnedSub.error) {
throw new ServiceError(ٌٌُERRORS.SOMETHING_WENT_WRONG_WITH_LS);
throw new ServiceError(ERRORS.SOMETHING_WENT_WRONG_WITH_LS);
}
// Triggers `onSubscriptionResume` event.
await this.eventPublisher.emitAsync(

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

@@ -2,6 +2,7 @@ import { Model, mixin } from 'objection';
import SystemModel from '@/system/models/SystemModel';
import moment from 'moment';
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
import { SubscriptionPaymentStatus } from '@/interfaces';
export default class PlanSubscription extends mixin(SystemModel) {
public lemonSubscriptionId: number;
@@ -13,6 +14,8 @@ export default class PlanSubscription extends mixin(SystemModel) {
public trialEndsAt: Date;
public paymentStatus: SubscriptionPaymentStatus;
/**
* Table name.
*/
@@ -31,7 +34,16 @@ export default class PlanSubscription extends mixin(SystemModel) {
* Defined virtual attributes.
*/
static get virtualAttributes() {
return ['active', 'inactive', 'ended', 'canceled', 'onTrial', 'status'];
return [
'active',
'inactive',
'ended',
'canceled',
'onTrial',
'status',
'isPaymentFailed',
'isPaymentSucceed',
];
}
/**
@@ -69,6 +81,22 @@ export default class PlanSubscription extends mixin(SystemModel) {
builder.where('trial_ends_at', '<=', endDate);
},
/**
* Filter the failed payment.
* @param builder
*/
failedPayment(builder) {
builder.where('payment_status', SubscriptionPaymentStatus.Failed);
},
/**
* Filter the succeed payment.
* @param builder
*/
succeedPayment(builder) {
builder.where('payment_status', SubscriptionPaymentStatus.Succeed);
},
};
}
@@ -108,10 +136,13 @@ export default class PlanSubscription extends mixin(SystemModel) {
/**
* Check if the subscription is active.
* Crtiria should be active:
* - During the trial period should NOT be canceled.
* - Out of trial period should NOT be ended.
* @return {Boolean}
*/
public active() {
return this.onTrial() || !this.ended();
return this.onTrial() ? !this.canceled() : !this.ended();
}
/**
@@ -200,4 +231,20 @@ export default class PlanSubscription extends mixin(SystemModel) {
);
return this.$query().update({ startsAt, endsAt });
}
/**
* Detarmines the subscription payment whether is failed.
* @returns {boolean}
*/
public isPaymentFailed() {
return this.paymentStatus === SubscriptionPaymentStatus.Failed;
}
/**
* Detarmines the subscription payment whether is succeed.
* @returns {boolean}
*/
public isPaymentSucceed() {
return this.paymentStatus === SubscriptionPaymentStatus.Succeed;
}
}

View File

@@ -15,12 +15,8 @@ export default class SubscriptionRepository extends SystemRepository {
* @param {number} tenantId
*/
getBySlugInTenant(slug: string, tenantId: number) {
const cacheKey = this.getCacheKey('getBySlugInTenant', slug, tenantId);
return this.cache.get(cacheKey, () => {
return PlanSubscription.query()
.findOne('slug', slug)
.where('tenant_id', tenantId);
});
return PlanSubscription.query()
.findOne('slug', slug)
.where('tenant_id', tenantId);
}
}

View File

@@ -1,4 +1,5 @@
// @ts-nocheck
import { lazy, Suspense } from 'react';
import { Router, Switch, Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { QueryClientProvider, QueryClient } from 'react-query';
@@ -11,26 +12,27 @@ import 'moment/locale/es-us';
import AppIntlLoader from './AppIntlLoader';
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
import { Authentication } from '@/containers/Authentication/Authentication';
import LazyLoader from '@/components/LazyLoader';
import { SplashScreen, DashboardThemeProvider } from '../components';
import { queryConfig } from '../hooks/query/base';
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
import { EnsureOneClickDemoAccountEnabled } from '@/containers/OneClickDemo/EnsureOneClickDemoAccountEnabled';
const EmailConfirmation = LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'),
});
const RegisterVerify = LazyLoader({
loader: () => import('@/containers/Authentication/RegisterVerify'),
});
const OneClickDemoPage = LazyLoader({
loader: () => import('@/containers/OneClickDemo/OneClickDemoPage'),
});
const DashboardPrivatePages = lazy(
() => import('@/components/Dashboard/PrivatePages'),
);
const AuthenticationPage = lazy(
() => import('@/containers/Authentication/AuthenticationPage'),
);
const EmailConfirmation = lazy(
() => import('@/containers/Authentication/EmailConfirmation'),
);
const RegisterVerify = lazy(
() => import('@/containers/Authentication/RegisterVerify'),
);
const OneClickDemoPage = lazy(
() => import('@/containers/OneClickDemo/OneClickDemoPage'),
);
/**
* App inner.
*/
@@ -38,42 +40,27 @@ function AppInsider({ history }) {
return (
<div className="App">
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/one_click_demo'}>
<EnsureOneClickDemoAccountEnabled>
<EnsureAuthNotAuthenticated>
<OneClickDemoPage />
</EnsureAuthNotAuthenticated>
</EnsureOneClickDemoAccountEnabled>
</Route>
<Route path={'/auth/register/verify'}>
<EnsureAuthenticated>
<EnsureUserEmailNotVerified>
<RegisterVerify />
</EnsureUserEmailNotVerified>
</EnsureAuthenticated>
</Route>
<Suspense fallback={'Loading...'}>
<Router history={history}>
<Switch>
<Route path={'/one_click_demo'} children={<OneClickDemoPage />} />
<Route path={'/auth/register/verify'}>
<EnsureAuthenticated>
<EnsureUserEmailNotVerified>
<RegisterVerify />
</EnsureUserEmailNotVerified>
</EnsureAuthenticated>
</Route>
<Route path={'/auth/email_confirmation'}>
<EmailConfirmation />
</Route>
<Route path={'/auth'}>
<EnsureAuthNotAuthenticated>
<Authentication />
</EnsureAuthNotAuthenticated>
</Route>
<Route path={'/'}>
<EnsureAuthenticated>
<EnsureUserEmailVerified>
<DashboardPrivatePages />
</EnsureUserEmailVerified>
</EnsureAuthenticated>
</Route>
</Switch>
</Router>
<Route
path={'/auth/email_confirmation'}
children={<EmailConfirmation />}
/>
<Route path={'/auth'} children={<AuthenticationPage />} />
<Route path={'/'} children={<DashboardPrivatePages />} />
</Switch>
</Router>
</Suspense>
<GlobalErrors />
</DashboardThemeProvider>

View File

@@ -1,35 +1,37 @@
// @ts-nocheck
import React from 'react';
import React, { lazy } from 'react';
import { Switch, Route } from 'react-router';
import Dashboard from '@/components/Dashboard/Dashboard';
import SetupWizardPage from '@/containers/Setup/WizardSetupPage';
import EnsureOrganizationIsReady from '../Guards/EnsureOrganizationIsReady';
import EnsureOrganizationIsNotReady from '../Guards/EnsureOrganizationIsNotReady';
import { PrivatePagesProvider } from './PrivatePagesProvider';
import EnsureOrganizationIsReady from '../Guards/EnsureOrganizationIsReady';
import { EnsureAuthenticated } from '../Guards/EnsureAuthenticated';
import { EnsureUserEmailVerified } from '../Guards/EnsureUserEmailVerified';
import '@/style/pages/Dashboard/Dashboard.scss';
const SetupWizardPage = lazy(
() => import('@/containers/Setup/WizardSetupPage'),
);
/**
* Dashboard inner private pages.
*/
export default function DashboardPrivatePages() {
return (
<PrivatePagesProvider>
<Switch>
<Route path={'/setup'}>
<EnsureOrganizationIsNotReady>
<SetupWizardPage />
</EnsureOrganizationIsNotReady>
</Route>
<Route path="/">
<EnsureOrganizationIsReady>
<Dashboard />
</EnsureOrganizationIsReady>
</Route>
</Switch>
</PrivatePagesProvider>
<EnsureAuthenticated>
<EnsureUserEmailVerified>
<PrivatePagesProvider>
<Switch>
<Route path={'/setup'} children={<SetupWizardPage />} />
<Route path="/">
<EnsureOrganizationIsReady>
<Dashboard />
</EnsureOrganizationIsReady>
</Route>
</Switch>
</PrivatePagesProvider>
</EnsureUserEmailVerified>
</EnsureAuthenticated>
);
}

View File

@@ -1,6 +1,7 @@
// @ts-nocheck
import React from 'react';
import { useApplicationBoot } from '@/components';
import { useAuthMetadata } from '@/hooks/query/authentication';
/**
* Private pages provider.
@@ -9,7 +10,10 @@ export function PrivatePagesProvider({
// #ownProps
children,
}) {
const { isLoading } = useApplicationBoot();
const { isLoading: isAppBootLoading } = useApplicationBoot();
const { isLoading: isAuthMetaLoading } = useAuthMetadata();
const isLoading = isAppBootLoading || isAuthMetaLoading;
return <React.Fragment>{!isLoading ? children : null}</React.Fragment>;
}

View File

@@ -1,22 +0,0 @@
// @ts-nocheck
import * as React from 'react';
import * as Loadable from 'react-loadable';
const Loader = (config) =>
Loadable({
loading: (props) => {
if (props.error) {
/* tslint:disable */
console.error(`======= DefaultLoader Error =======`);
console.error(props.error);
console.error(`======= DefaultLoader Error =======`);
/* tslint:enable */
return null;
}
return null;
},
delay: 250,
...config
});
export default Loader;

View File

@@ -1,21 +1,33 @@
// @ts-nocheck
import React from 'react';
import React, { Suspense } from 'react';
import { Route, Switch } from 'react-router-dom';
import preferencesRoutes from '@/routes/preferences';
import { getPreferenceRoutes } from '@/routes/preferences';
import { Spinner } from '@blueprintjs/core';
import { Box } from '../Layout';
export default function DashboardContentRoute() {
const preferencesRoutes = getPreferenceRoutes();
return (
<Route pathname="/preferences">
<Switch>
{preferencesRoutes.map((route, index) => (
<Route
key={index}
path={`${route.path}`}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
<Suspense
fallback={
<Box style={{ padding: 20 }}>
<Spinner size={20} />
</Box>
}
>
<Switch>
{preferencesRoutes.map((route, index) => (
<Route
key={index}
path={`${route.path}`}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</Suspense>
</Route>
);
}

View File

@@ -1,6 +0,0 @@
export const Config = {
oneClickDemo: {
enable: process.env.REACT_APP_ONE_CLICK_DEMO_ENABLE === 'true',
demoUrl: process.env.REACT_APP_DEMO_ACCOUNT_URL || '',
},
};

View File

@@ -2,10 +2,12 @@
import { Route, Switch, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import styled from 'styled-components';
import { Suspense } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { Spinner } from '@blueprintjs/core';
import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components';
import { Box, Icon, FormattedMessage as T } from '@/components';
import { AuthMetaBootProvider } from './AuthMetaBoot';
import '@/style/pages/Authentication/Auth.scss';
@@ -20,7 +22,15 @@ export function Authentication() {
</AuthLogo>
<AuthMetaBootProvider>
<AuthenticationRoutes />
<Suspense
fallback={
<Box style={{ marginTop: '5rem' }}>
<Spinner size={30} />
</Box>
}
>
<AuthenticationRoutes />
</Suspense>
</AuthMetaBootProvider>
</AuthInsider>
</AuthPage>

View File

@@ -0,0 +1,10 @@
import { EnsureAuthNotAuthenticated } from "@/components/Guards/EnsureAuthNotAuthenticated";
import { Authentication } from "./Authentication";
export default function AuthenticationPage() {
return (
<EnsureAuthNotAuthenticated>
<Authentication />
</EnsureAuthNotAuthenticated>
);
}

View File

@@ -9,9 +9,11 @@ import {
compose,
updateTableCell,
updateAutoAddNewLine,
updateMinEntriesLines,
orderingLinesIndexes,
updateTableRow,
formattedAmount,
updateRemoveLineByIndex,
} from '@/utils';
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';

View File

@@ -77,6 +77,15 @@ function GlobalErrors({
},
});
}
if (globalErrors.subscriptionInactive) {
AppToaster.show({
message: `You can't add new data to Bigcapital because your subscription is inactive. Make sure your billing information is up-to-date from Preferences > Billing page.`,
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ subscriptionInactive: false });
},
});
}
if (globalErrors.userInactive) {
AppToaster.show({
message: intl.get('global_error.authorized_user_inactive'),

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { Config } from '@/config';
import { useOneClickDemoBoot } from './OneClickDemoBoot';
interface EnsureOneClickDemoAccountEnabledProps {
children: React.ReactNode;
@@ -11,9 +11,10 @@ export const EnsureOneClickDemoAccountEnabled = ({
children,
redirectTo = '/',
}: EnsureOneClickDemoAccountEnabledProps) => {
const enabeld = Config.oneClickDemo.enable || false;
const { authMeta } = useOneClickDemoBoot();
const enabled = authMeta?.meta?.one_click_demo?.enable || false;
if (!enabeld) {
if (!enabled) {
return <Redirect to={{ pathname: redirectTo }} />;
}
return <>{children}</>;

View File

@@ -0,0 +1,45 @@
import React, { createContext, useContext, ReactNode } from 'react';
import { useAuthMetadata } from '@/hooks/query/authentication';
interface OneClickDemoContextType {
authMeta: any;
}
const OneClickDemoContext = createContext<OneClickDemoContextType>(
{} as OneClickDemoContextType,
);
export const useOneClickDemoBoot = () => {
const context = useContext(OneClickDemoContext);
if (!context) {
throw new Error(
'useOneClickDemo must be used within a OneClickDemoProvider',
);
}
return context;
};
interface OneClickDemoBootProps {
children: ReactNode;
}
export const OneClickDemoBoot: React.FC<OneClickDemoBootProps> = ({
children,
}) => {
const { isLoading: isAuthMetaLoading, data: authMeta } = useAuthMetadata();
const value = {
isAuthMetaLoading,
authMeta,
};
if (isAuthMetaLoading) {
return null;
}
return (
<OneClickDemoContext.Provider value={value}>
{children}
</OneClickDemoContext.Provider>
);
};

View File

@@ -1,97 +1,16 @@
// @ts-nocheck
import { Button, Intent, ProgressBar, Text } from '@blueprintjs/core';
import { useEffect, useState } from 'react';
import {
useCreateOneClickDemo,
useOneClickDemoSignin,
} from '@/hooks/query/oneclick-demo';
import { Box, Icon, Stack } from '@/components';
import { useJob } from '@/hooks/query';
import style from './OneClickDemoPage.module.scss';
import { EnsureAuthNotAuthenticated } from '@/components/Guards/EnsureAuthNotAuthenticated';
import { EnsureOneClickDemoAccountEnabled } from './EnsureOneClickDemoAccountEnabled';
import { OneClickDemoBoot } from './OneClickDemoBoot';
import { OneClickDemoPageContent } from './OneClickDemoPageContent';
export default function OneClickDemoPage() {
const {
mutateAsync: createOneClickDemo,
isLoading: isCreateOneClickLoading,
} = useCreateOneClickDemo();
const {
mutateAsync: oneClickDemoSignIn,
isLoading: isOneclickDemoSigningIn,
} = useOneClickDemoSignin();
// Job states.
const [demoId, setDemoId] = useState<string>('');
const [buildJobId, setBuildJobId] = useState<string>('');
const [isJobDone, setIsJobDone] = useState<boolean>(false);
const {
data: { running, completed },
} = useJob(buildJobId, {
refetchInterval: 2000,
enabled: !isJobDone && !!buildJobId,
});
useEffect(() => {
if (completed) {
setIsJobDone(true);
}
}, [completed, setIsJobDone]);
// One the job done request sign-in using the demo id.
useEffect(() => {
if (isJobDone) {
oneClickDemoSignIn({ demoId }).then((res) => {
debugger;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isJobDone]);
const handleCreateAccountBtnClick = () => {
createOneClickDemo({})
.then(({ data: { data } }) => {
setBuildJobId(data?.build_job?.job_id);
setDemoId(data?.demo_id);
})
.catch(() => {});
};
const isLoading = running || isOneclickDemoSigningIn;
return (
<Box className={style.root}>
<Box className={style.inner}>
<Stack align={'center'} spacing={40}>
<Icon icon="bigcapital" height={37} width={228} />
{isLoading && (
<Stack align={'center'} spacing={15}>
<ProgressBar stripes value={null} className={style.progressBar} />
{isOneclickDemoSigningIn && (
<Text className={style.waitingText}>
It's signin-in to your demo account, Just a second!
</Text>
)}
{running && (
<Text className={style.waitingText}>
We're preparing temporary environment for trial, It typically
take few seconds. Do not close or refresh the page.
</Text>
)}
</Stack>
)}
</Stack>
{!isLoading && (
<Button
className={style.oneClickBtn}
intent={Intent.NONE}
onClick={handleCreateAccountBtnClick}
loading={isCreateOneClickLoading}
>
Create Demo Account
</Button>
)}
</Box>
</Box>
<EnsureAuthNotAuthenticated>
<OneClickDemoBoot>
<EnsureOneClickDemoAccountEnabled>
<OneClickDemoPageContent />
</EnsureOneClickDemoAccountEnabled>
</OneClickDemoBoot>
</EnsureAuthNotAuthenticated>
);
}

View File

@@ -0,0 +1,98 @@
// @ts-nocheck
import { Button, Intent, ProgressBar, Text } from '@blueprintjs/core';
import { useEffect, useState } from 'react';
import {
useCreateOneClickDemo,
useOneClickDemoSignin,
} from '@/hooks/query/oneclick-demo';
import { Box, Icon, Stack } from '@/components';
import { useJob } from '@/hooks/query';
import style from './OneClickDemoPage.module.scss';
export function OneClickDemoPageContent() {
const {
mutateAsync: createOneClickDemo,
isLoading: isCreateOneClickLoading,
} = useCreateOneClickDemo();
const {
mutateAsync: oneClickDemoSignIn,
isLoading: isOneclickDemoSigningIn,
} = useOneClickDemoSignin();
// Job states.
const [demoId, setDemoId] = useState<string>('');
const [buildJobId, setBuildJobId] = useState<string>('');
const [isJobDone, setIsJobDone] = useState<boolean>(false);
const {
data: { running, completed },
} = useJob(buildJobId, {
refetchInterval: 2000,
enabled: !isJobDone && !!buildJobId,
});
useEffect(() => {
if (completed) {
setIsJobDone(true);
}
}, [completed, setIsJobDone]);
// One the job done request sign-in using the demo id.
useEffect(() => {
if (isJobDone) {
oneClickDemoSignIn({ demoId }).then((res) => {
debugger;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isJobDone]);
const handleCreateAccountBtnClick = () => {
createOneClickDemo({})
.then(({ data: { data } }) => {
setBuildJobId(data?.build_job?.job_id);
setDemoId(data?.demo_id);
})
.catch(() => {});
};
const isLoading = running || isOneclickDemoSigningIn;
return (
<Box className={style.root}>
<Box className={style.inner}>
<Stack align={'center'} spacing={40}>
<Icon icon="bigcapital" height={37} width={228} />
{isLoading && (
<Stack align={'center'} spacing={15}>
<ProgressBar stripes value={null} className={style.progressBar} />
{isOneclickDemoSigningIn && (
<Text className={style.waitingText}>
It's signin-in to your demo account, Just a second!
</Text>
)}
{running && (
<Text className={style.waitingText}>
We're preparing the temporary environment for trial. It
typically takes a few seconds. Do not close or refresh the
page.
</Text>
)}
</Stack>
)}
</Stack>
{!isLoading && (
<Button
className={style.oneClickBtn}
intent={Intent.NONE}
onClick={handleCreateAccountBtnClick}
loading={isCreateOneClickLoading}
>
Create Demo Account
</Button>
)}
</Box>
</Box>
);
}

View File

@@ -6,7 +6,7 @@ import { Icon, For, FormattedMessage as T, Stack } from '@/components';
import { getFooterLinks } from '@/constants/footerLinks';
import { useAuthActions } from '@/hooks/state';
import style from './SetupLeftSection.module.scss';
import { Config } from '@/config';
import { useAuthMetadata } from '@/hooks/query';
/**
* Footer item link.
@@ -28,13 +28,16 @@ function SetupLeftSectionFooter() {
// Retrieve the footer links.
const footerLinks = getFooterLinks();
const { data: authMeta } = useAuthMetadata();
const demoUrl = authMeta?.meta?.one_click_demo?.demo_url;
const handleDemoBtnClick = () => {
window.open(Config.oneClickDemo.demoUrl);
window.open(demoUrl);
};
return (
<div className={'content__footer'}>
{Config.oneClickDemo.demoUrl && (
{demoUrl && (
<Stack spacing={16}>
<Text className={style.demoButtonLabel}>Not Now?</Text>
<button className={style.demoButton} onClick={handleDemoBtnClick}>

View File

@@ -0,0 +1,20 @@
.container {
text-align: center;
margin-bottom: 1.15rem;
}
.iconText {
display: inline-flex;
font-size: 14px;
margin-right: 16px;
color: #00824d;
&:last-child {
margin-right: 0; /* Remove the margin on the last item */
}
}
.icon {
margin-right: 2px;
}

View File

@@ -0,0 +1,35 @@
import styles from './SubscriptionPlansOfferChecks.module.scss';
export function SubscriptionPlansOfferChecks() {
return (
<div className={styles.container}>
<span className={styles.iconText}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
viewBox="0 -960 960 960"
fill="rgb(0, 130, 77)"
className={styles.icon}
>
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
</svg>
14-day free trial
</span>
<span className={styles.iconText}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
viewBox="0 -960 960 960"
fill="rgb(0, 130, 77)"
className={styles.icon}
>
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
</svg>
24/7 online support
</span>
</div>
);
}

View File

@@ -24,7 +24,7 @@ function SubscriptionPlansPeriodSwitcherRoot({
);
};
return (
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}>
<Group position={'center'} spacing={10} style={{ marginBottom: '1.6rem' }}>
<Text>Pay Monthly</Text>
<Switch
large

View File

@@ -1,6 +1,7 @@
import { Callout } from '@blueprintjs/core';
import { SubscriptionPlans } from './SubscriptionPlans';
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
import { SubscriptionPlansOfferChecks } from './SubscriptionPlansOfferChecks';
/**
* Billing plans.
@@ -14,6 +15,7 @@ export function SubscriptionPlansSection() {
include applicable taxes.
</Callout>
<SubscriptionPlansOfferChecks />
<SubscriptionPlansPeriodSwitcher />
<SubscriptionPlans />
</section>

View File

@@ -2,14 +2,17 @@
import React from 'react';
import SetupRightSection from './SetupRightSection';
import SetupLeftSection from './SetupLeftSection';
import EnsureOrganizationIsNotReady from '@/components/Guards/EnsureOrganizationIsNotReady';
import '@/style/pages/Setup/SetupPage.scss';
export default function WizardSetupPage() {
return (
<div class="setup-page">
<SetupLeftSection />
<SetupRightSection />
</div>
<EnsureOrganizationIsNotReady>
<div class="setup-page">
<SetupLeftSection />
<SetupRightSection />
</div>
</EnsureOrganizationIsNotReady>
);
};
}

View File

@@ -100,7 +100,7 @@ export const useAuthResetPassword = (props) => {
/**
* Fetches the authentication page metadata.
*/
export const useAuthMetadata = (props) => {
export const useAuthMetadata = (props = {}) => {
return useRequestQuery(
[t.AUTH_METADATA_PAGE],
{

View File

@@ -64,12 +64,20 @@ export default function useApiRequest() {
setGlobalErrors({ too_many_requests: true });
}
if (status === 400) {
const lockedError = data.errors.find(
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
);
if (lockedError) {
if (
data.errors.find(
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
)
) {
setGlobalErrors({ transactionsLocked: { ...lockedError.data } });
}
if (
data.errors.find(
(e) => e.type === 'ORGANIZATION.SUBSCRIPTION.INACTIVE',
)
) {
setGlobalErrors({ subscriptionInactive: true });
}
if (data.errors.find((e) => e.type === 'USER_INACTIVE')) {
setGlobalErrors({ userInactive: true });
setLogout();

View File

@@ -1,43 +1,35 @@
// @ts-nocheck
import LazyLoader from '@/components/LazyLoader';
import { lazy } from 'react';
const BASE_URL = '/auth';
export default [
{
path: `${BASE_URL}/login`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/Login'),
}),
component: lazy(() => import('@/containers/Authentication/Login')),
},
{
path: `${BASE_URL}/send_reset_password`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/SendResetPassword'),
}),
component: lazy(
() => import('@/containers/Authentication/SendResetPassword'),
),
},
{
path: `${BASE_URL}/reset_password/:token`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/ResetPassword'),
}),
component: lazy(() => import('@/containers/Authentication/ResetPassword')),
},
{
path: `${BASE_URL}/invite/:token/accept`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/InviteAccept'),
}),
component: lazy(() => import('@/containers/Authentication/InviteAccept')),
},
{
path: `${BASE_URL}/register/email_confirmation`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'),
}),
component: lazy(
() => import('@/containers/Authentication/EmailConfirmation'),
),
},
{
path: `${BASE_URL}/register`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/Register'),
}),
component: lazy(() => import('@/containers/Authentication/Register')),
},
];

View File

@@ -1,88 +1,96 @@
// @ts-nocheck
import { lazy } from 'react';
import General from '@/containers/Preferences/General/General';
import Users from '../containers/Preferences/Users/Users';
import Roles from '../containers/Preferences/Users/Roles/RolesForm/RolesFormPage';
import Accountant from '@/containers/Preferences/Accountant/Accountant';
import Currencies from '@/containers/Preferences/Currencies/Currencies';
import Item from '@/containers/Preferences/Item';
// import SMSIntegration from '../containers/Preferences/SMSIntegration';
import DefaultRoute from '../containers/Preferences/DefaultRoute';
import Warehouses from '../containers/Preferences/Warehouses';
import Branches from '../containers/Preferences/Branches';
import Invoices from '../containers/Preferences/Invoices/PreferencesInvoices';
import { PreferencesCreditNotes } from '../containers/Preferences/CreditNotes/PreferencesCreditNotes';
import { PreferencesEstimates } from '@/containers/Preferences/Estimates/PreferencesEstimates';
import { PreferencesReceipts } from '@/containers/Preferences/Receipts/PreferencesReceipts';
import BillingPage from '@/containers/Subscriptions/BillingPage';
const BASE_URL = '/preferences';
export default [
export const getPreferenceRoutes = () => [
{
path: `${BASE_URL}/general`,
component: General,
component: lazy(() => import('@/containers/Preferences/General/General')),
exact: true,
},
{
path: `${BASE_URL}/users`,
component: Users,
component: lazy(() => import('../containers/Preferences/Users/Users')),
exact: true,
},
{
path: `${BASE_URL}/invoices`,
component: Invoices,
component: lazy(
() => import('../containers/Preferences/Invoices/PreferencesInvoices'),
),
exact: true,
},
{
path: `${BASE_URL}/credit-notes`,
component: PreferencesCreditNotes,
component: lazy(() =>
import(
'../containers/Preferences/CreditNotes/PreferencesCreditNotes'
).then((module) => ({ default: module.PreferencesCreditNotes })),
),
exact: true,
},
{
path: `${BASE_URL}/estimates`,
component: PreferencesEstimates,
component: lazy(() =>
import('@/containers/Preferences/Estimates/PreferencesEstimates').then(
(module) => ({ default: module.PreferencesEstimates }),
),
),
exact: true,
},
{
path: `${BASE_URL}/receipts`,
component: PreferencesReceipts,
component: lazy(() =>
import('@/containers/Preferences/Receipts/PreferencesReceipts').then(
(module) => ({ default: module.PreferencesReceipts }),
),
),
exact: true,
},
{
path: `${BASE_URL}/roles`,
component: Roles,
component: lazy(
() =>
import('../containers/Preferences/Users/Roles/RolesForm/RolesFormPage'),
),
exact: true,
},
{
path: `${BASE_URL}/roles/:id`,
component: Roles,
component: lazy(
() =>
import('../containers/Preferences/Users/Roles/RolesForm/RolesFormPage'),
),
exact: true,
},
{
path: `${BASE_URL}/currencies`,
component: Currencies,
component: lazy(
() => import('@/containers/Preferences/Currencies/Currencies'),
),
exact: true,
},
{
path: `${BASE_URL}/warehouses`,
component: Warehouses,
component: lazy(() => import('../containers/Preferences/Warehouses')),
exact: true,
},
{
path: `${BASE_URL}/branches`,
component: Branches,
component: lazy(() => import('../containers/Preferences/Branches')),
exact: true,
},
{
path: `${BASE_URL}/accountant`,
component: Accountant,
component: lazy(
() => import('@/containers/Preferences/Accountant/Accountant'),
),
exact: true,
},
{
path: `${BASE_URL}/items`,
component: Item,
component: lazy(() => import('@/containers/Preferences/Item')),
exact: true,
},
// {
@@ -92,12 +100,12 @@ export default [
// },
{
path: `${BASE_URL}/billing`,
component: BillingPage,
component: lazy(() => import('@/containers/Subscriptions/BillingPage')),
exact: true,
},
{
path: `${BASE_URL}/`,
component: DefaultRoute,
component: lazy(() => import('../containers/Preferences/DefaultRoute')),
exact: true,
},
];

View File

@@ -29,7 +29,7 @@
&__content {
width: 100%;
padding-bottom: 40px;
padding-bottom: 80px;
}
&__left-section {

36
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
async:
specifier: ^3.2.0
version: 3.2.5
async-mutex:
specifier: ^0.5.0
version: 0.5.0
axios:
specifier: ^1.6.0
version: 1.7.2
@@ -269,6 +272,9 @@ importers:
pluralize:
specifier: ^8.0.0
version: 8.0.0
posthog-node:
specifier: ^4.2.0
version: 4.2.0
pug:
specifier: ^3.0.2
version: 3.0.2
@@ -8009,6 +8015,12 @@ packages:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
dev: false
/async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
dependencies:
tslib: 2.6.2
dev: false
/async-settle@1.0.0:
resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==}
engines: {node: '>= 0.10'}
@@ -8090,6 +8102,16 @@ packages:
transitivePeerDependencies:
- debug
/axios@1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
dependencies:
follow-redirects: 1.15.6
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/axobject-query@3.2.1:
resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==}
dependencies:
@@ -20468,6 +20490,16 @@ packages:
picocolors: 1.0.1
source-map-js: 1.2.0
/posthog-node@4.2.0:
resolution: {integrity: sha512-hgyCYMyzMvuF3qWMw6JvS8gT55v7Mtp5wKWcnDrw+nu39D0Tk9BXD7I0LOBp0lGlHEPaXCEVYUtviNKrhMALGA==}
engines: {node: '>=15.0.0'}
dependencies:
axios: 1.7.7
rusha: 0.8.14
transitivePeerDependencies:
- debug
dev: false
/prebuildify@6.0.1:
resolution: {integrity: sha512-8Y2oOOateom/s8dNBsGIcnm6AxPmLH4/nanQzL5lQMU+sC0CMhzARZHizwr36pUPLdvBnOkCNQzxg4djuFSgIw==}
hasBin: true
@@ -22722,6 +22754,10 @@ packages:
engines: {node: '>=14'}
dev: true
/rusha@0.8.14:
resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==}
dev: false
/rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
dependencies: