Compare commits

..

2 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
0517b3e430 Merge branch 'develop' into add-mail-invoice-receipt-schema 2024-11-10 14:37:11 +02:00
Ahmed Bouhuolia
4c67d2e321 feat: add invoice mail receipt schema 2024-11-06 14:17:28 +02:00
24 changed files with 131 additions and 195 deletions

View File

@@ -121,7 +121,7 @@ export default class BillsController extends BaseController {
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.discount') check('entries.*.discount')
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()

View File

@@ -170,7 +170,7 @@ export default class VendorCreditController extends BaseController {
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.discount') check('entries.*.discount')
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
@@ -209,7 +209,7 @@ export default class VendorCreditController extends BaseController {
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.discount') check('entries.*.discount')
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()

View File

@@ -233,7 +233,7 @@ export default class PaymentReceivesController extends BaseController {
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.discount') check('entries.*.discount')
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
@@ -755,8 +755,9 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const data = const data = await this.getCreditNoteStateService.getCreditNoteState(
await this.getCreditNoteStateService.getCreditNoteState(tenantId); tenantId
);
return res.status(200).send({ data }); return res.status(200).send({ data });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -172,7 +172,7 @@ export default class SalesEstimatesController extends BaseController {
check('entries').exists().isArray({ min: 1 }), check('entries').exists().isArray({ min: 1 }),
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description').optional({ nullable: true }).trim(), check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.discount') check('entries.*.discount')
@@ -562,8 +562,9 @@ export default class SalesEstimatesController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const data = const data = await this.saleEstimatesApplication.getSaleEstimateState(
await this.saleEstimatesApplication.getSaleEstimateState(tenantId); tenantId
);
return res.status(200).send({ data }); return res.status(200).send({ data });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -148,7 +148,7 @@ export default class SalesReceiptsController extends BaseController {
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.discount') check('entries.*.discount')
.optional({ nullable: true }) .optional({ nullable: true })
@@ -392,8 +392,9 @@ export default class SalesReceiptsController extends BaseController {
// Retrieves receipt in pdf format. // Retrieves receipt in pdf format.
try { try {
const data = const data = await this.saleReceiptsApplication.getSaleReceiptState(
await this.saleReceiptsApplication.getSaleReceiptState(tenantId); tenantId
);
return res.status(200).send({ data }); return res.status(200).send({ data });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -102,13 +102,12 @@ export class PublicSharableLinkController extends BaseController {
const { paymentLinkId } = req.params; const { paymentLinkId } = req.params;
try { try {
const [pdfContent, filename] = const pdfContent = await this.paymentLinkApp.getPaymentLinkInvoicePdf(
await this.paymentLinkApp.getPaymentLinkInvoicePdf(paymentLinkId); paymentLinkId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
} catch (error) { } catch (error) {

View File

@@ -1,39 +0,0 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema
.table('items_entries', (table) => {
table.decimal('quantity', 13, 3).alter();
})
.table('inventory_transactions', (table) => {
table.decimal('quantity', 13, 3).alter();
})
.table('inventory_cost_lot_tracker', (table) => {
table.decimal('quantity', 13, 3).alter();
})
.table('items', (table) => {
table.decimal('quantityOnHand', 13, 3).alter();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema
.table('items_entries', (table) => {
table.integer('quantity').alter();
})
.table('inventory_transactions', (table) => {
table.integer('quantity').alter();
})
.table('inventory_cost_lot_tracker', (table) => {
table.integer('quantity').alter();
})
.table('items', (table) => {
table.integer('quantityOnHand').alter();
});
};

View File

@@ -25,7 +25,7 @@ export class GetInvoicePaymentLinkMetadata {
.findOne('linkId', linkId) .findOne('linkId', linkId)
.where('resourceType', 'SaleInvoice') .where('resourceType', 'SaleInvoice')
.throwIfNotFound(); .throwIfNotFound();
// Validate the expiry at date. // Validate the expiry at date.
if (paymentLink.expiryAt) { if (paymentLink.expiryAt) {
const currentDate = moment(); const currentDate = moment();
@@ -46,7 +46,6 @@ export class GetInvoicePaymentLinkMetadata {
.withGraphFetched('customer') .withGraphFetched('customer')
.withGraphFetched('taxes.taxRate') .withGraphFetched('taxes.taxRate')
.withGraphFetched('paymentMethods.paymentIntegration') .withGraphFetched('paymentMethods.paymentIntegration')
.withGraphFetched('pdfTemplate')
.throwIfNotFound(); .throwIfNotFound();
return this.transformer.transform( return this.transformer.transform(

View File

@@ -12,11 +12,9 @@ export class GetPaymentLinkInvoicePdf {
* Retrieves the sale invoice PDF of the given payment link id. * Retrieves the sale invoice PDF of the given payment link id.
* @param {number} tenantId * @param {number} tenantId
* @param {number} paymentLinkId * @param {number} paymentLinkId
* @returns {Promise<Buffer, string>} * @returns {Promise<Buffer>}
*/ */
async getPaymentLinkInvoicePdf( async getPaymentLinkInvoicePdf(paymentLinkId: string): Promise<Buffer> {
paymentLinkId: string
): Promise<[Buffer, string]> {
const paymentLink = await PaymentLink.query() const paymentLink = await PaymentLink.query()
.findOne('linkId', paymentLinkId) .findOne('linkId', paymentLinkId)
.where('resourceType', 'SaleInvoice') .where('resourceType', 'SaleInvoice')

View File

@@ -11,7 +11,7 @@ export class PaymentLinksApplication {
@Inject() @Inject()
private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
@Inject() @Inject()
private getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf; private getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf;
@@ -45,9 +45,7 @@ export class PaymentLinksApplication {
* @param {number} paymentLinkId * @param {number} paymentLinkId
* @returns {Promise<Buffer> } * @returns {Promise<Buffer> }
*/ */
public getPaymentLinkInvoicePdf( public getPaymentLinkInvoicePdf(paymentLinkId: string): Promise<Buffer> {
paymentLinkId: string
): Promise<[Buffer, string]> {
return this.getPaymentLinkInvoicePdfService.getPaymentLinkInvoicePdf( return this.getPaymentLinkInvoicePdfService.getPaymentLinkInvoicePdf(
paymentLinkId paymentLinkId
); );

View File

@@ -4,7 +4,6 @@ import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer'; import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { contactAddressTextFormat } from '@/utils/address-text-format'; import { contactAddressTextFormat } from '@/utils/address-text-format';
import { GetPdfTemplateTransformer } from '@/services/PdfTemplate/GetPdfTemplateTransformer';
export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer { export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer {
/** /**
@@ -46,7 +45,6 @@ export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer
'isReceivable', 'isReceivable',
'hasStripePaymentMethod', 'hasStripePaymentMethod',
'formattedCustomerAddress', 'formattedCustomerAddress',
'brandingTemplate',
]; ];
}; };
@@ -65,18 +63,6 @@ export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer
); );
} }
/**
* Retrieves the branding template for the payment link.
* @param {} invoice
* @returns
*/
public brandingTemplate(invoice) {
return this.item(
invoice.pdfTemplate,
new GetInvoicePaymentLinkBrandingTemplate()
);
}
/** /**
* Retrieves the entries of the sale invoice. * Retrieves the entries of the sale invoice.
* @param {ISaleInvoice} invoice * @param {ISaleInvoice} invoice
@@ -128,7 +114,7 @@ export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer
/** /**
* Retrieves the formatted customer address. * Retrieves the formatted customer address.
* @param invoice * @param invoice
* @returns {string} * @returns {string}
*/ */
protected formattedCustomerAddress(invoice) { protected formattedCustomerAddress(invoice) {
@@ -207,17 +193,3 @@ class GetInvoicePaymentLinkTaxEntryTransformer extends SaleInvoiceTaxEntryTransf
return ['name', 'taxRateCode', 'taxRateAmount', 'taxRateAmountFormatted']; return ['name', 'taxRateCode', 'taxRateAmount', 'taxRateAmountFormatted'];
}; };
} }
class GetInvoicePaymentLinkBrandingTemplate extends GetPdfTemplateTransformer {
public includeAttributes = (): string[] => {
return ['companyLogoUri', 'primaryColor'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
primaryColor = (template) => {
return template.attributes?.primaryColor;
};
}

View File

@@ -1,6 +1,8 @@
// @ts-nocheck
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { FormGroup, NumericInput, Intent } from '@blueprintjs/core'; import { FormGroup, NumericInput, Intent } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { CellType } from '@/constants'; import { CellType } from '@/constants';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
@@ -10,44 +12,37 @@ import { CLASSES } from '@/constants/classes';
export default function NumericInputCell({ export default function NumericInputCell({
row: { index }, row: { index },
column: { id }, column: { id },
cell: { value: controlledInputValue }, cell: { value: initialValue },
payload, payload,
}: any) { }) {
const [valueAsNumber, setValueAsNumber] = useState<number | null>( const [value, setValue] = useState(initialValue);
controlledInputValue || null,
);
const handleInputValueChange = (
valueAsNumber: number,
valueAsString: string,
) => {
setValueAsNumber(valueAsNumber);
};
const handleInputBlur = () => {
payload.updateData(index, id, valueAsNumber);
};
const handleValueChange = (newValue) => {
setValue(newValue);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => { useEffect(() => {
setValueAsNumber(controlledInputValue); setValue(initialValue);
// eslint-disable-next-line react-hooks/exhaustive-deps }, [initialValue]);
}, [controlledInputValue]);
const error = payload.errors?.[index]?.[id]; const error = payload.errors?.[index]?.[id];
return ( return (
<FormGroup <FormGroup
intent={error ? Intent.DANGER : undefined} intent={error ? Intent.DANGER : null}
className={classNames(CLASSES.FILL)} className={classNames(CLASSES.FILL)}
> >
<NumericInput <NumericInput
asyncControl value={value}
value={controlledInputValue} onValueChange={handleValueChange}
onValueChange={handleInputValueChange} onBlur={onBlur}
onBlur={handleInputBlur} fill={true}
buttonPosition={'none'} buttonPosition={'none'}
fill
/> />
</FormGroup> </FormGroup>
); );
} }
NumericInputCell.cellType = CellType.Field; NumericInputCell.cellType = CellType.Field;

View File

@@ -4,7 +4,7 @@ import * as R from 'ramda';
import moment from 'moment'; import moment from 'moment';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { sumBy, setWith, get, first, toNumber } from 'lodash'; import { sumBy, setWith, toSafeInteger, get, first } from 'lodash';
import { import {
updateTableCell, updateTableCell,
repeatValue, repeatValue,
@@ -91,8 +91,8 @@ export function transformToEditForm(manualJournal) {
* Entries adjustment. * Entries adjustment.
*/ */
function adjustmentEntries(entries) { function adjustmentEntries(entries) {
const credit = sumBy(entries, (e) => toNumber(e.credit)); const credit = sumBy(entries, (e) => toSafeInteger(e.credit));
const debit = sumBy(entries, (e) => toNumber(e.debit)); const debit = sumBy(entries, (e) => toSafeInteger(e.debit));
return { return {
debit: Math.max(credit - debit, 0), debit: Math.max(credit - debit, 0),

View File

@@ -22,7 +22,7 @@ interface AttachmentFileCommon {
size: number; size: number;
mimeType: string; mimeType: string;
} }
interface AttachmentFileLoaded extends AttachmentFileCommon { } interface AttachmentFileLoaded extends AttachmentFileCommon {}
interface AttachmentFileLoading extends AttachmentFileCommon { interface AttachmentFileLoading extends AttachmentFileCommon {
loading: boolean; loading: boolean;
} }
@@ -74,11 +74,11 @@ export function UploadAttachmentsPopoverContent({
}; };
// Uploads the attachments. // Uploads the attachments.
const { mutateAsync: uploadAttachments } = useUploadAttachments({ const { mutateAsync: uploadAttachments } = useUploadAttachments({
onSuccess: (data, formData) => { onSuccess: (data) => {
const newLocalFiles = stopLoadingAttachment( const newLocalFiles = stopLoadingAttachment(
localFiles, localFiles,
formData.get('internalKey'), data.config.data.get('internalKey'),
data.key, data.data.data.key,
); );
handleFilesChange(newLocalFiles); handleFilesChange(newLocalFiles);
onUploadedChange && onUploadedChange(newLocalFiles); onUploadedChange && onUploadedChange(newLocalFiles);

View File

@@ -13,6 +13,7 @@ import {
Tag, Tag,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { import {
FormatNumberCell,
TextOverviewTooltipCell, TextOverviewTooltipCell,
FormattedMessage as T, FormattedMessage as T,
Choose, Choose,
@@ -50,8 +51,9 @@ export const useBillReadonlyEntriesTableColumns = () => {
}, },
{ {
Header: intl.get('quantity'), Header: intl.get('quantity'),
accessor: 'quantity_formatted', accessor: 'quantity',
width: getColumnWidth(entries, 'quantity_formatted', { Cell: FormatNumberCell,
width: getColumnWidth(entries, 'quantity', {
minWidth: 60, minWidth: 60,
magicSpacing: 5, magicSpacing: 5,
}), }),

View File

@@ -48,8 +48,9 @@ export const useCreditNoteReadOnlyEntriesColumns = () => {
}, },
{ {
Header: intl.get('quantity'), Header: intl.get('quantity'),
accessor: 'quantity_formatted', accessor: 'quantity',
width: getColumnWidth(entries, 'quantity_formatted', { Cell: FormatNumberCell,
width: getColumnWidth(entries, 'quantity', {
minWidth: 60, minWidth: 60,
magicSpacing: 5, magicSpacing: 5,
}), }),

View File

@@ -54,10 +54,11 @@ export const useInvoiceReadonlyEntriesColumns = () => {
{ {
Header: intl.get('quantity'), Header: intl.get('quantity'),
accessor: 'quantity', accessor: 'quantity',
Cell: FormatNumberCell,
align: 'right', align: 'right',
disableSortBy: true, disableSortBy: true,
textOverview: true, textOverview: true,
width: getColumnWidth(entries, 'quantity_formatted', { width: getColumnWidth(entries, 'quantity', {
minWidth: 60, minWidth: 60,
magicSpacing: 5, magicSpacing: 5,
}), }),

View File

@@ -72,7 +72,7 @@ export const useEstimateTransactionsColumns = () => {
{ {
id: 'qunatity', id: 'qunatity',
Header: intl.get('item.drawer_quantity_sold'), Header: intl.get('item.drawer_quantity_sold'),
accessor: 'quantity_formatted', accessor: 'quantity',
align: 'right', align: 'right',
width: 100, width: 100,
}, },

View File

@@ -31,8 +31,9 @@ export const useReceiptReadonlyEntriesTableColumns = () => {
}, },
{ {
Header: intl.get('quantity'), Header: intl.get('quantity'),
accessor: 'quantity_formatted', accessor: 'quantity',
width: getColumnWidth(entries, 'quantity_formatted', { Cell: FormatNumberCell,
width: getColumnWidth(entries, 'quantity', {
minWidth: 60, minWidth: 60,
magicSpacing: 5, magicSpacing: 5,
}), }),

View File

@@ -1,11 +1,6 @@
:root {
--payment-page-background-color: #fff;
--payment-page-primary-button: #0052cc;
}
.rootBodyPage { .rootBodyPage {
background: var(--payment-page-background-color); background: #1c1d29;
} }
.root { .root {

View File

@@ -1,6 +1,5 @@
import { Text, Classes, Button, Intent } from '@blueprintjs/core'; import { Text, Classes, Button, Intent } from '@blueprintjs/core';
import clsx from 'classnames'; import clsx from 'classnames';
import { css } from '@emotion/css';
import { AppToaster, Box, Group, Stack } from '@/components'; import { AppToaster, Box, Group, Stack } from '@/components';
import { usePaymentPortalBoot } from './PaymentPortalBoot'; import { usePaymentPortalBoot } from './PaymentPortalBoot';
import { useDrawerActions } from '@/hooks/state'; import { useDrawerActions } from '@/hooks/state';
@@ -67,11 +66,11 @@ export function PaymentPortal() {
<Stack spacing={0} className={styles.body}> <Stack spacing={0} className={styles.body}>
<Stack> <Stack>
<Group spacing={10}> <Group spacing={10}>
{sharableLinkMeta?.brandingTemplate?.companyLogoUri && ( {sharableLinkMeta?.organization?.logoUri && (
<Box <Box
className={styles.companyLogoWrap} className={styles.companyLogoWrap}
style={{ style={{
backgroundImage: `url(${sharableLinkMeta?.brandingTemplate?.companyLogoUri})`, backgroundImage: `url(${sharableLinkMeta?.organization?.logoUri})`,
}} }}
></Box> ></Box>
)} )}
@@ -171,22 +170,7 @@ export function PaymentPortal() {
sharableLinkMeta?.hasStripePaymentMethod && ( sharableLinkMeta?.hasStripePaymentMethod && (
<Button <Button
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
className={clsx( className={clsx(styles.footerButton, styles.buyButton)}
styles.footerButton,
styles.buyButton,
css`
&.bp4-button.bp4-intent-primary {
background-color: var(--payment-page-primary-button);
&:hover,
&:focus {
background-color: var(
--payment-page-primary-button-hover
);
}
}
`,
)}
loading={isStripeCheckoutLoading} loading={isStripeCheckoutLoading}
onClick={handlePayButtonClick} onClick={handlePayButtonClick}
> >

View File

@@ -6,8 +6,6 @@ import { PaymentPortalBoot, usePaymentPortalBoot } from './PaymentPortalBoot';
import { PaymentInvoicePreviewDrawer } from './drawers/PaymentInvoicePreviewDrawer/PaymentInvoicePreviewDrawer'; import { PaymentInvoicePreviewDrawer } from './drawers/PaymentInvoicePreviewDrawer/PaymentInvoicePreviewDrawer';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import styles from './PaymentPortal.module.scss'; import styles from './PaymentPortal.module.scss';
import { useEffect } from 'react';
import { hsl, lighten, parseToHsl } from 'polished';
export default function PaymentPortalPage() { export default function PaymentPortalPage() {
const { linkId } = useParams<{ linkId: string }>(); const { linkId } = useParams<{ linkId: string }>();
@@ -16,7 +14,6 @@ export default function PaymentPortalPage() {
<BodyClassName className={styles.rootBodyPage}> <BodyClassName className={styles.rootBodyPage}>
<PaymentPortalBoot linkId={linkId}> <PaymentPortalBoot linkId={linkId}>
<PaymentPortalHelmet /> <PaymentPortalHelmet />
<PaymentPortalCssVariables />
<PaymentPortal /> <PaymentPortal />
<PaymentInvoicePreviewDrawer name={DRAWERS.PAYMENT_INVOICE_PREVIEW} /> <PaymentInvoicePreviewDrawer name={DRAWERS.PAYMENT_INVOICE_PREVIEW} />
</PaymentPortalBoot> </PaymentPortalBoot>
@@ -39,33 +36,3 @@ function PaymentPortalHelmet() {
</Helmet> </Helmet>
); );
} }
/**
* Renders the dynamic CSS variables for the current payment page.
* @returns {React.ReactNode}
*/
function PaymentPortalCssVariables() {
const { sharableLinkMeta } = usePaymentPortalBoot();
useEffect(() => {
if (sharableLinkMeta?.brandingTemplate?.primaryColor) {
const primaryColorHsl = parseToHsl(
sharableLinkMeta?.brandingTemplate?.primaryColor,
);
document.body.style.setProperty(
'--payment-page-background-color',
hsl(primaryColorHsl.hue, 0.19, 0.14),
);
document.body.style.setProperty(
'--payment-page-primary-button',
sharableLinkMeta?.brandingTemplate?.primaryColor,
);
document.body.style.setProperty(
'--payment-page-primary-button-hover',
lighten(0.05, sharableLinkMeta?.brandingTemplate?.primaryColor),
);
}
}, [sharableLinkMeta?.brandingTemplate?.primaryColor]);
return null;
}

View File

@@ -106,10 +106,6 @@ export interface GetInvoicePaymentLinkResponse {
taxRateAmountFormatted: string; taxRateAmountFormatted: string;
taxRateCode: string; taxRateCode: string;
}>; }>;
brandingTemplate: {
companyLogoUri: string;
primaryColor: string;
};
organization: GetInvoicePaymentLinkOrganizationRes; organization: GetInvoicePaymentLinkOrganizationRes;
hasStripePaymentMethod: boolean; hasStripePaymentMethod: boolean;
isReceivable: boolean; isReceivable: boolean;

View File

@@ -0,0 +1,64 @@
export interface InvoicePaymentEmailSchemaProps {
companyName: string;
companyLogoUri: string;
total: string;
currencyCode: string;
dueDate: string;
paymentUrl: string;
invoiceNumber: string;
invoiceDate: string;
}
export function InvoicePaymentEmailSchema({
companyName,
companyLogoUri,
currencyCode,
total,
paymentUrl,
dueDate,
invoiceDate,
invoiceNumber
}: InvoicePaymentEmailSchemaProps) {
return (
<script type="application/ld+json">
{`
{
"@context": "http://schema.org",
"@type": "Invoice",
"description": "Invoice for services rendered",
"paymentStatus": "http://schema.org/PaymentDue",
"totalPaymentDue": {
"@type": "MonetaryAmount",
"currency": "${currencyCode}",
"value": "${total}"
},
"provider": {
"@type": "Organization",
"name": "${companyName}",
"logo": "${companyLogoUri}",
"telephone": "+1234567890"
},
"paymentDueDate": "${dueDate}",
"paymentUrl": "${paymentUrl}",
"referencesOrder": {
"@type": "Order",
"orderNumber": "${invoiceNumber}",
"orderStatus": "http://schema.org/OrderPaymentDue",
"orderDate": "${invoiceDate}"
},
"potentialAction": {
"@type": "ViewAction",
"target": "${paymentUrl}",
"name": "View and Pay Invoice"
}
}
`}
</script>
);
}