Compare commits

...

3 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
fc6ebfea5c Debounce scheduling calculating items cost 2024-08-28 21:25:47 +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
11 changed files with 120 additions and 41 deletions

View File

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

View File

@@ -139,24 +139,26 @@ export default class InventoryService {
) { ) {
const agenda = Container.get('agenda'); 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 // Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item. // with the same given item.
await agenda.cancel({ await agenda.cancel({
name: 'compute-item-cost', ...commonJobsQuery,
nextRunAt: { $ne: null }, 'data.startingDate': { $lte: startingDate },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $gt: startingDate },
}); });
// Retrieve any `compute-item-cost` in the queue has lower starting date // Retrieve any `compute-item-cost` in the queue has lower starting date
// with the same given item. // with the same given item.
const dependsJobs = await agenda.jobs({ const dependsJobs = await agenda.jobs({
name: 'compute-item-cost', ...commonJobsQuery,
nextRunAt: { $ne: null }, 'data.startingDate': { $gte: startingDate },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $lte: startingDate },
}); });
// If the depends jobs cleared.
if (dependsJobs.length === 0) { if (dependsJobs.length === 0) {
await agenda.schedule( await agenda.schedule(
config.scheduleComputeItemCost, config.scheduleComputeItemCost,

View File

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

View File

@@ -86,20 +86,14 @@ export default class InventorySubscriber {
private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({ private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({
tenantId, tenantId,
inventoryTransactions, inventoryTransactions,
trx trx,
}: IInventoryTransactionsCreatedPayload) => { }: IInventoryTransactionsCreatedPayload) => {
const inventoryItemsIds = map(inventoryTransactions, 'itemId'); const inventoryItemsIds = map(inventoryTransactions, 'itemId');
runAfterTransaction(trx, async () => { await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
try { tenantId,
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions( inventoryTransactions
tenantId, );
inventoryTransactions
);
} catch (error) {
console.error(error);
}
});
}; };
/** /**

View File

@@ -6,6 +6,7 @@ import {
ISaleInvoiceEditedPayload, ISaleInvoiceEditedPayload,
} from '@/interfaces'; } from '@/interfaces';
import { SaleInvoiceGLEntries } from '@/services/Sales/Invoices/InvoiceGLEntries'; import { SaleInvoiceGLEntries } from '@/services/Sales/Invoices/InvoiceGLEntries';
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
@Service() @Service()
export default class SaleInvoiceWriteGLEntriesSubscriber { export default class SaleInvoiceWriteGLEntriesSubscriber {

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 ( return (
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}> <Group position={'center'} spacing={10} style={{ marginBottom: '1.6rem' }}>
<Text>Pay Monthly</Text> <Text>Pay Monthly</Text>
<Switch <Switch
large large

View File

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

View File

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

9
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
async: async:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.5 version: 3.2.5
async-mutex:
specifier: ^0.5.0
version: 0.5.0
axios: axios:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.7.2 version: 1.7.2
@@ -8009,6 +8012,12 @@ packages:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
dev: false 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: /async-settle@1.0.0:
resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==} resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}