Compare commits

...

9 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
6b6b73b77c feat: send signup event to Loops (#531)
* feat: send signup event to Loops

* feat: fix
2024-07-17 15:56:05 +02:00
Ahmed Bouhuolia
107a6f793b Merge pull request #526 from bigcapitalhq/monthly-plans
feat: upgrade the subscription plans
2024-07-14 14:21:57 +02:00
Ahmed Bouhuolia
67d155759e feat: backend the new monthly susbcription plans 2024-07-14 14:19:04 +02:00
Ahmed Bouhuolia
7e2e87256f Merge pull request #527 from bigcapitalhq/fix-sync-removed-transactions
fix: sync the removed bank transactions from the source
2024-07-13 21:56:13 +02:00
Ahmed Bouhuolia
df7790d7c1 fix: sync the removed bank transactions from the source 2024-07-13 21:54:44 +02:00
Ahmed Bouhuolia
72128a72c4 feat: add variant ids to new subscription plans 2024-07-13 19:53:52 +02:00
Ahmed Bouhuolia
eb3f23554f feat: upgrade the subscription plans 2024-07-13 18:19:18 +02:00
Ahmed Bouhuolia
69ddf43b3e fix: duplicated event emitter 2024-07-13 03:23:25 +02:00
Ahmed Bouhuolia
249eadaeaa Merge pull request #525 from bigcapitalhq/fix-plaid-transactions-syncing
fix: Plaid transactions syncing
2024-07-12 23:44:27 +02:00
23 changed files with 634 additions and 182 deletions

View File

@@ -237,4 +237,8 @@ module.exports = {
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
},
loops: {
apiKey: process.env.LOOPS_API_KEY,
},
};

View File

@@ -113,6 +113,7 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
export default () => {
return new EventPublisher();
@@ -274,5 +275,8 @@ export const susbcribers = () => {
// Plaid
RecognizeSyncedBankTranasctions,
// Loops
LoopsEventsSubscriber
];
};

View File

@@ -73,8 +73,6 @@ export class PlaidUpdateTransactions {
added.concat(modified),
trx
);
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
// Sync transactions cursor.
await this.plaidSync.syncTransactionsCursor(
tenantId,

View File

@@ -37,7 +37,7 @@ export class CreateUncategorizedTransaction {
tenantId,
async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizedCreated,
events.cashflow.onTransactionUncategorizedCreating,
{
tenantId,
createUncategorizedTransactionDTO,

View File

@@ -0,0 +1,51 @@
import axios from 'axios';
import config from '@/config';
import { IAuthSignUpVerifiedEventPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { SystemUser } from '@/system/models';
export class LoopsEventsSubscriber {
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.auth.signUpConfirmed,
this.triggerEventOnSignupVerified.bind(this)
);
}
/**
* Once the user verified sends the event to the Loops.
* @param {IAuthSignUpVerifiedEventPayload} param0
*/
public async triggerEventOnSignupVerified({
email,
userId,
}: IAuthSignUpVerifiedEventPayload) {
// Can't continue since the Loops the api key is not configured.
if (!config.loops.apiKey) {
return;
}
const user = await SystemUser.query().findById(userId);
const options = {
method: 'POST',
url: 'https://app.loops.so/api/v1/events/send',
headers: {
Authorization: `Bearer ${config.loops.apiKey}`,
'Content-Type': 'application/json',
},
data: {
email,
userId,
firstName: user.firstName,
lastName: user.lastName,
eventName: 'USER_VERIFIED',
eventProperties: {},
mailingLists: {},
},
};
await axios(options);
}
}

View File

@@ -1,4 +1,3 @@
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
import config from '@/config';
import { Inject, Service } from 'typedi';
import {
@@ -10,7 +9,6 @@ import {
} from './utils';
import { Plan } from '@/system/models';
import { Subscription } from './Subscription';
import { isEmpty } from 'lodash';
@Service()
export class LemonSqueezyWebhooks {
@@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks {
private subscriptionService: Subscription;
/**
* handle the LemonSqueezy webhooks.
* Handles the Lemon Squeezy webhooks.
* @param {string} rawBody
* @param {string} signature
* @returns {Promise<void>}
@@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks {
const variantId = attributes.variant_id as string;
// We assume that the Plan table is up to date.
const plan = await Plan.query().findOne('slug', 'early-adaptor');
const plan = await Plan.query().findOne('lemonVariantId', variantId);
if (!plan) {
throw new Error(`Plan with variantId ${variantId} not found.`);
@@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks {
// Update the subscription in the database.
const priceId = attributes.first_subscription_item.price_id;
// Get the price data from Lemon Squeezy.
const priceData = await getPrice(priceId);
if (priceData.error) {
throw new Error(
`Failed to get the price data for the subscription ${eventBody.data.id}.`
);
}
const isUsageBased =
attributes.first_subscription_item.is_usage_based;
const price = isUsageBased
? priceData.data?.data.attributes.unit_price_decimal
: priceData.data?.data.attributes.unit_price;
// Create a new subscription of the tenant.
if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion(
tenantId,
'early-adaptor'
);
await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
}
}
} else if (webhookEvent.startsWith('order_')) {

View File

@@ -40,6 +40,13 @@ export default {
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
},
/**
* User subscription events.
*/
subscription: {
onSubscribed: 'onOrganizationSubscribed',
},
/**
* Tenants managment service.
*/

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('subscription_plans', (table) => {
table.string('lemon_variant_id').nullable().index();
});
};
exports.down = (knex) => {
return knex.schema.table('subscription_plans', (table) => {
table.dropColumn('lemon_variant_id');
});
};

View File

@@ -0,0 +1,96 @@
exports.up = function (knex) {
return knex('subscription_plans').insert([
// Capital Basic
{
name: 'Capital Basic (Monthly)',
slug: 'capital-basic-monthly',
price: 10,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446152',
// lemon_variant_id: '450016',
},
{
name: 'Capital Basic (Annually)',
slug: 'capital-basic-annually',
price: 90,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446153',
// lemon_variant_id: '450018',
},
// # Capital Essential
{
name: 'Capital Essential (Monthly)',
slug: 'capital-essential-monthly',
price: 20,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446155',
// lemon_variant_id: '450028',
},
{
name: 'Capital Essential (Annually)',
slug: 'capital-essential-annually',
price: 180,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446156',
// lemon_variant_id: '450029',
},
// # Capital Plus
{
name: 'Capital Plus (Monthly)',
slug: 'capital-plus-monthly',
price: 25,
active: true,
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446165',
// lemon_variant_id: '450031',
},
{
name: 'Capital Plus (Annually)',
slug: 'capital-plus-annually',
price: 228,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446164',
// lemon_variant_id: '450032',
},
// # Capital Big
{
name: 'Capital Big (Monthly)',
slug: 'capital-big-monthly',
price: 40,
active: true,
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446167',
// lemon_variant_id: '450024',
},
{
name: 'Capital Big (Annually)',
slug: 'capital-big-annually',
price: 360,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446168',
// lemon_variant_id: '450025',
},
]);
};
exports.down = function (knex) {};

View File

@@ -23,9 +23,10 @@
color: #fff;
text-align: center;
font-size: 12px;
text-transform: uppercase;
}
.label {
font-size: 14px;
font-size: 16px;
font-weight: 600;
color: #2F343C;
@@ -47,13 +48,31 @@
}
.price {
font-size: 18px;
line-height: 1;
font-weight: 500;
color: #404854;
line-height: 1;
font-weight: 500;
color: #252A31;
}
.pricePer{
color: #738091;
font-size: 12px;
line-height: 1;
}
.featureItem{
flex: 1;
color: #1C2127;
}
.featurePopover :global .bp4-popover-content{
border-radius: 0;
}
.featurePopoverContent{
font-size: 12px
}
.featurePopoverLabel {
text-transform: uppercase;
letter-spacing: 0.4px;
font-size: 12px;
font-weight: 500;
}

View File

@@ -1,4 +1,11 @@
import { Button, ButtonProps, Intent } from '@blueprintjs/core';
import {
Button,
ButtonProps,
Intent,
Position,
Text,
Tooltip,
} from '@blueprintjs/core';
import clsx from 'classnames';
import { Box, Group, Stack } from '../Layout';
import styles from './PricingPlan.module.scss';
@@ -64,7 +71,7 @@ export interface PricingPriceProps {
*/
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
return (
<Stack spacing={6} className={styles.priceRoot}>
<Stack spacing={4} className={styles.priceRoot}>
<h4 className={styles.price}>{price}</h4>
<span className={styles.pricePer}>{subPrice}</span>
</Stack>
@@ -101,7 +108,7 @@ export interface PricingFeaturesProps {
*/
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
return (
<Stack spacing={10} className={styles.features}>
<Stack spacing={14} className={styles.features}>
{children}
</Stack>
);
@@ -109,15 +116,41 @@ PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
export interface PricingFeatureLineProps {
children: React.ReactNode;
hintContent?: string;
hintLabel?: string;
}
/**
* Displays a single feature line within a list of features.
* @param children - The content of the feature line.
*/
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
return (
<Group noWrap spacing={12}>
PricingPlan.FeatureLine = ({
children,
hintContent,
hintLabel,
}: PricingFeatureLineProps) => {
return hintContent ? (
<Tooltip
content={
<Stack spacing={5}>
{hintLabel && (
<Text className={styles.featurePopoverLabel}>{hintLabel}</Text>
)}
<Text className={styles.featurePopoverContent}>{hintContent}</Text>
</Stack>
}
position={Position.TOP_LEFT}
popoverClassName={styles.featurePopover}
modifiers={{ offset: { enabled: true, offset: '0,10' } }}
minimal
>
<Group noWrap spacing={8} style={{ cursor: 'help' }}>
<CheckCircled height={12} width={12} />
<Box className={styles.featureItem}>{children}</Box>
</Group>
</Tooltip>
) : (
<Group noWrap spacing={8}>
<CheckCircled height={12} width={12} />
<Box className={styles.featureItem}>{children}</Box>
</Group>

View File

@@ -1,10 +1,140 @@
// @ts-nocheck
// Subscription plans.
export const plans = [
];
interface SubscriptionPlanFeature {
text: string;
hint?: string;
label?: string;
style?: Record<string, string>;
}
interface SubscriptionPlan {
name: string;
slug: string;
description: string;
features: SubscriptionPlanFeature[];
featured?: boolean;
monthlyPrice: string;
monthlyPriceLabel: string;
annuallyPrice: string;
annuallyPriceLabel: string;
monthlyVariantId: string;
annuallyVariantId: string;
}
// Payment methods.
export const paymentMethods = [
];
export const SubscriptionPlans = [
{
name: 'Capital Basic',
slug: 'capital_basic',
description: 'Good for service businesses that just started.',
features: [
{
text: 'Unlimited Sale Invoices',
hintLabel: 'Unlimited Sale Invoices',
hint: 'Good for service businesses that just started for service businesses that just started',
},
{ text: 'Unlimated Sale Estimates' },
{ text: 'Track GST and VAT' },
{ text: 'Connect Banks for Automatic Importing' },
{ text: 'Chart of Accounts' },
{
text: 'Manual Journals',
hintLabel: 'Manual Journals',
hint: 'Write manual journals entries for financial transactions not automatically captured by the system to adjust financial statements.',
},
{
text: 'Basic Financial Reports & Insights',
hint: 'Balance sheet, profit & loss statement, cashflow statement, general ledger, journal sheet, A/P aging summary, A/R aging summary',
},
{ text: 'Unlimited User Seats' },
],
monthlyPrice: '$10',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$7.5',
annuallyPriceLabel: 'Per month',
monthlyVariantId: '446152',
// monthlyVariantId: '450016',
annuallyVariantId: '446153',
// annuallyVariantId: '450018',
},
{
name: 'Capital Essential',
slug: 'capital_plus',
description: 'Good for have inventory and want more financial reports.',
features: [
{ text: 'All Capital Basic features' },
{ text: 'Purchase Invoices' },
{
text: 'Multi Currency Transactions',
hintLabel: 'Multi Currency',
hint: 'Pay and get paid and do manual journals in any currency with real time exchange rates conversions.',
},
{
text: 'Transactions Locking',
hintLabel: 'Transactions Locking',
hint: 'Transaction Locking freezes transactions to prevent any additions, modifications, or deletions of transactions recorded during the specified date.',
},
{
text: 'Inventory Tracking',
hintLabel: 'Inventory Tracking',
hint: 'Track goods in the stock, cost of goods, and get notifications when quantity is low.',
},
{ text: 'Smart Financial Reports' },
{ text: 'Advanced Inventory Reports' },
],
monthlyPrice: '$20',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$15',
annuallyPriceLabel: 'Per month',
// monthlyVariantId: '450028',
monthlyVariantId: '446155',
// annuallyVariantId: '450029',
annuallyVariantId: '446156',
},
{
name: 'Capital Plus',
slug: 'essentials',
description: 'Good for business want financial and access control.',
features: [
{ text: 'All Capital Essential features' },
{ text: 'Custom User Roles Access' },
{ text: 'Vendor Credits' },
{
text: 'Budgeting',
hint: 'Create multiple budgets and compare targets with actuals to understand how your business is performing.',
},
{ text: 'Analysis Cost Center' },
],
monthlyPrice: '$25',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$19',
annuallyPriceLabel: 'Per month',
featured: true,
// monthlyVariantId: '450031',
monthlyVariantId: '446165',
// annuallyVariantId: '450032',
annuallyVariantId: '446164',
},
{
name: 'Capital Big',
slug: 'essentials',
description: 'Good for businesses have multiple branches.',
features: [
{ text: 'All Capital Plus features' },
{
text: 'Multiple Branches',
hintLabel: '',
hint: 'Track the organization transactions and accounts in multiple branches.',
},
{
text: 'Multiple Warehouses',
hintLabel: 'Multiple Warehouses',
hint: 'Track the organization inventory in multiple warehouses and transfer goods between them.',
},
],
monthlyPrice: '$40',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$30',
annuallyPriceLabel: 'Per month',
// monthlyVariantId: '450024',
monthlyVariantId: '446167',
// annuallyVariantId: '450025',
annuallyVariantId: '446168',
},
] as SubscriptionPlan[];

View File

@@ -3,3 +3,7 @@
margin: 0 auto;
padding: 0 40px;
}
.periodSwitch {
margin: 0;
}

View File

@@ -1,32 +1,65 @@
// @ts-nocheck
import { AppToaster, Group, T } from '@/components';
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
import { Intent } from '@blueprintjs/core';
import * as R from 'ramda';
import { AppToaster } from '@/components';
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
import { PricingPlan } from '@/components/PricingPlan/PricingPlan';
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
import {
WithPlansProps,
withPlans,
} from '@/containers/Subscriptions/withPlans';
interface SubscriptionPricingFeature {
text: string;
hint?: string;
hintLabel?: string;
style?: Record<string, string>;
}
interface SubscriptionPricingProps {
slug: string;
label: string;
description: string;
features?: Array<String>;
features?: Array<SubscriptionPricingFeature>;
featured?: boolean;
price: string;
pricePeriod: string;
monthlyPrice: string;
monthlyPriceLabel: string;
annuallyPrice: string;
annuallyPriceLabel: string;
monthlyVariantId?: string;
annuallyVariantId?: string;
}
function SubscriptionPricing({
featured,
interface SubscriptionPricingCombinedProps
extends SubscriptionPricingProps,
WithPlansProps {}
function SubscriptionPlanRoot({
label,
description,
featured,
features,
price,
pricePeriod,
}: SubscriptionPricingProps) {
monthlyPrice,
monthlyPriceLabel,
annuallyPrice,
annuallyPriceLabel,
monthlyVariantId,
annuallyVariantId,
// #withPlans
plansPeriod,
}: SubscriptionPricingCombinedProps) {
const { mutateAsync: getLemonCheckout, isLoading } =
useGetLemonSqueezyCheckout();
const handleClick = () => {
getLemonCheckout({ variantId: '338516' })
const variantId =
SubscriptionPlansPeriod.Monthly === plansPeriod
? monthlyVariantId
: annuallyVariantId;
getLemonCheckout({ variantId })
.then((res) => {
const checkoutUrl = res.data.data.attributes.url;
window.LemonSqueezy.Url.Open(checkoutUrl);
@@ -42,37 +75,34 @@ function SubscriptionPricing({
return (
<PricingPlan featured={featured}>
{featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>}
<PricingPlan.Header label={label} description={description} />
<PricingPlan.Price price={price} subPrice={pricePeriod} />
{plansPeriod === SubscriptionPlansPeriod.Monthly ? (
<PricingPlan.Price price={monthlyPrice} subPrice={monthlyPriceLabel} />
) : (
<PricingPlan.Price
price={annuallyPrice}
subPrice={annuallyPriceLabel}
/>
)}
<PricingPlan.BuyButton loading={isLoading} onClick={handleClick}>
Subscribe
</PricingPlan.BuyButton>
<PricingPlan.Features>
{features?.map((feature) => (
<PricingPlan.FeatureLine>{feature}</PricingPlan.FeatureLine>
<PricingPlan.FeatureLine
hintLabel={feature.hintLabel}
hintContent={feature.hint}
>
{feature.text}
</PricingPlan.FeatureLine>
))}
</PricingPlan.Features>
</PricingPlan>
);
}
export function SubscriptionPlans({ plans }) {
return (
<Group spacing={18} noWrap align='stretch'>
{plans.map((plan, index) => (
<SubscriptionPricing
key={index}
slug={plan.slug}
label={plan.name}
description={plan.description}
features={plan.features}
featured={plan.featured}
price={plan.price}
pricePeriod={plan.pricePeriod}
/>
))}
</Group>
);
}
export const SubscriptionPlan = R.compose(
withPlans(({ plansPeriod }) => ({ plansPeriod })),
)(SubscriptionPlanRoot);

View File

@@ -0,0 +1,28 @@
import { Group } from '@/components';
import { SubscriptionPlan } from './SubscriptionPlan';
import { useSubscriptionPlans } from './hooks';
export function SubscriptionPlans() {
const subscriptionPlans = useSubscriptionPlans();
return (
<Group spacing={14} noWrap align="stretch">
{subscriptionPlans.map((plan, index) => (
<SubscriptionPlan
key={index}
slug={plan.slug}
label={plan.name}
description={plan.description}
features={plan.features}
featured={plan.featured}
monthlyPrice={plan.monthlyPrice}
monthlyPriceLabel={plan.monthlyPriceLabel}
annuallyPrice={plan.annuallyPrice}
annuallyPriceLabel={plan.annuallyPriceLabel}
monthlyVariantId={plan.monthlyVariantId}
annuallyVariantId={plan.annuallyVariantId}
/>
))}
</Group>
);
}

View File

@@ -0,0 +1,46 @@
import { ChangeEvent } from 'react';
import * as R from 'ramda';
import { Intent, Switch, Tag, Text } from '@blueprintjs/core';
import { Group } from '@/components';
import withSubscriptionPlansActions, {
WithSubscriptionPlansActionsProps,
} from '@/containers/Subscriptions/withSubscriptionPlansActions';
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
import styles from './SetupSubscription.module.scss';
interface SubscriptionPlansPeriodsSwitchCombinedProps
extends WithSubscriptionPlansActionsProps {}
function SubscriptionPlansPeriodSwitcherRoot({
// #withSubscriptionPlansActions
changeSubscriptionPlansPeriod,
}: SubscriptionPlansPeriodsSwitchCombinedProps) {
// Handles the period switch change.
const handleSwitchChange = (event: ChangeEvent<HTMLInputElement>) => {
changeSubscriptionPlansPeriod(
event.currentTarget.checked
? SubscriptionPlansPeriod.Annually
: SubscriptionPlansPeriod.Monthly,
);
};
return (
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}>
<Text>Pay Monthly</Text>
<Switch
large
onChange={handleSwitchChange}
className={styles.periodSwitch}
/>
<Text>
Pay Yearly{' '}
<Tag minimal intent={Intent.NONE}>
25% Off All Year
</Tag>
</Text>
</Group>
);
}
export const SubscriptionPlansPeriodSwitcher = R.compose(
withSubscriptionPlansActions,
)(SubscriptionPlansPeriodSwitcherRoot);

View File

@@ -1,29 +1,21 @@
// @ts-nocheck
import { Callout } from '@blueprintjs/core';
import { SubscriptionPlans } from './SubscriptionPlan';
import withPlans from '../../Subscriptions/withPlans';
import { compose } from '@/utils';
import { SubscriptionPlans } from './SubscriptionPlans';
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
/**
* Billing plans.
*/
function SubscriptionPlansSectionRoot({ plans }) {
export function SubscriptionPlansSection() {
return (
<section>
<Callout
style={{ marginBottom: '1.5rem' }}
icon={null}
title={'Early Adopter Plan'}
>
We're looking for 200 early adopters, when you subscribe you'll get the
full features and unlimited users for a year regardless of the
subscribed plan.
<Callout style={{ marginBottom: '2rem' }} icon={null}>
Simple plans. Simple prices. Only pay for what you really need. All
plans come with award-winning 24/7 customer support. Prices do not
include applicable taxes.
</Callout>
<SubscriptionPlans plans={plans} />
<SubscriptionPlansPeriodSwitcher />
<SubscriptionPlans />
</section>
);
}
export const SubscriptionPlansSection = compose(
withPlans(({ plans }) => ({ plans })),
)(SubscriptionPlansSectionRoot);

View File

@@ -0,0 +1,5 @@
import { SubscriptionPlans } from '@/constants/subscriptionModels';
export const useSubscriptionPlans = () => {
return SubscriptionPlans;
};

View File

@@ -1,17 +1,35 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { MapStateToProps, connect } from 'react-redux';
import {
getPlansPeriodSelector,
getPlansSelector,
} from '@/store/plans/plans.selectors';
import { ApplicationState } from '@/store/reducers';
export default (mapState) => {
const mapStateToProps = (state, props) => {
export interface WithPlansProps {
plans: ReturnType<ReturnType<typeof getPlansSelector>>;
plansPeriod: ReturnType<ReturnType<typeof getPlansPeriodSelector>>;
}
type MapState<Props> = (
mapped: WithPlansProps,
state: ApplicationState,
props: Props,
) => any;
export function withPlans<Props>(mapState?: MapState<Props>) {
const mapStateToProps: MapStateToProps<
WithPlansProps,
Props,
ApplicationState
> = (state, props) => {
const getPlans = getPlansSelector();
const getPlansPeriod = getPlansPeriodSelector();
const mapped = {
plans: getPlans(state, props),
plans: getPlans(state),
plansPeriod: getPlansPeriod(state),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};
}

View File

@@ -1,9 +1,22 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { initSubscriptionPlans } from '@/store/plans/plans.actions';
import { MapDispatchToProps, connect } from 'react-redux';
import {
SubscriptionPlansPeriod,
changePlansPeriod,
initSubscriptionPlans,
} from '@/store/plans/plans.reducer';
export const mapDispatchToProps = (dispatch) => ({
export interface WithSubscriptionPlansActionsProps {
initSubscriptionPlans: () => void;
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) => void;
}
export const mapDispatchToProps: MapDispatchToProps<
WithSubscriptionPlansActionsProps,
{}
> = (dispatch: any) => ({
initSubscriptionPlans: () => dispatch(initSubscriptionPlans()),
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) =>
dispatch(changePlansPeriod({ period })),
});
export default connect(null, mapDispatchToProps);
export default connect(null, mapDispatchToProps);

View File

@@ -1,70 +1,46 @@
// @ts-nocheck
import { createReducer } from '@reduxjs/toolkit';
import t from '@/store/types';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { SubscriptionPlans } from '@/constants/subscriptionModels';
const getSubscriptionPlans = () => [
{
name: 'Capital Basic',
slug: 'capital_basic',
description: 'Good for service businesses that just started.',
features: [
'Sale Invoices and Estimates',
'Tracking Expenses',
'Customize Invoice',
'Manual Journals',
'Bank Reconciliation',
'Chart of Accounts',
'Taxes',
'Basic Financial Reports & Insights',
],
price: '$29',
pricePeriod: 'Per Year',
},
{
name: 'Capital Plus',
slug: 'capital_plus',
description:
'Good for businesses have inventory and want more financial reports.',
features: [
'All Capital Basic features',
'Manage Bills',
'Inventory Tracking',
'Multi Currencies',
'Predefined user roles.',
'Transactions locking.',
'Smart Financial Reports.',
],
price: '$29',
pricePeriod: 'Per Year',
featured: true,
},
{
name: 'Capital Big',
slug: 'essentials',
description: 'Good for businesses have multiple inventory or branches.',
features: [
'All Capital Plus features',
'Multiple Warehouses',
'Multiple Branches',
'Invite >= 15 Users',
],
price: '$29',
pricePeriod: 'Per Year',
},
];
export enum SubscriptionPlansPeriod {
Monthly = 'monthly',
Annually = 'Annually',
}
const initialState = {
plans: [],
periods: [],
};
interface StorePlansState {
plans: any;
plansPeriod: SubscriptionPlansPeriod;
}
export default createReducer(initialState, {
/**
* Initialize the subscription plans.
*/
[t.INIT_SUBSCRIPTION_PLANS]: (state) => {
const plans = getSubscriptionPlans();
export const SubscriptionPlansSlice = createSlice({
name: 'plans',
initialState: {
plans: [],
periods: [],
plansPeriod: 'monthly',
} as StorePlansState,
reducers: {
/**
* Initialize the subscription plans.
* @param {StorePlansState} state
*/
initSubscriptionPlans: (state: StorePlansState) => {
const plans = SubscriptionPlans;
state.plans = plans;
},
state.plans = plans;
/**
* Changes the plans period (monthly or annually).
* @param {StorePlansState} state
* @param {PayloadAction<{ period: SubscriptionPlansPeriod }>} action
*/
changePlansPeriod: (
state: StorePlansState,
action: PayloadAction<{ period: SubscriptionPlansPeriod }>,
) => {
state.plansPeriod = action.payload.period;
},
},
});
export const { initSubscriptionPlans, changePlansPeriod } =
SubscriptionPlansSlice.actions;

View File

@@ -2,19 +2,21 @@
import { createSelector } from 'reselect';
const plansSelector = (state) => state.plans.plans;
const planSelector = (state, props) => state.plans.plans
.find((plan) => plan.slug === props.planSlug);
const planSelector = (state, props) =>
state.plans.plans.find((plan) => plan.slug === props.planSlug);
const plansPeriodSelector = (state) => state.plans.plansPeriod;
// Retrieve manual jounral current page results.
export const getPlansSelector = () => createSelector(
plansSelector,
(plans) => {
export const getPlansSelector = () =>
createSelector(plansSelector, (plans) => {
return plans;
},
);
});
// Retrieve plan details.
export const getPlanSelector = () => createSelector(
planSelector,
(plan) => plan,
)
export const getPlanSelector = () =>
createSelector(planSelector, (plan) => plan);
// Retrieves the plans period (monthly or annually).
export const getPlansPeriodSelector = () =>
createSelector(plansPeriodSelector, (periods) => periods);

View File

@@ -32,13 +32,17 @@ import paymentMades from './PaymentMades/paymentMades.reducer';
import organizations from './organizations/organizations.reducers';
import subscriptions from './subscription/subscription.reducer';
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
import plans from './plans/plans.reducer';
import { SubscriptionPlansSlice } from './plans/plans.reducer';
import creditNotes from './CreditNote/creditNote.reducer';
import vendorCredit from './VendorCredit/VendorCredit.reducer';
import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer';
import projects from './Project/projects.reducer';
import { PlaidSlice } from './banking/banking.reducer';
export interface ApplicationState {
}
const appReducer = combineReducers({
authentication,
organizations,
@@ -69,7 +73,7 @@ const appReducer = combineReducers({
paymentReceives,
paymentMades,
inventoryAdjustments,
plans,
plans: SubscriptionPlansSlice.reducer,
creditNotes,
vendorCredit,
warehouseTransfers,