Skip to content

Commit

Permalink
Merge pull request #506 from Pinelab-studio/feat/prevent-accounting-e…
Browse files Browse the repository at this point in the history
…xport

Invoices 4.1.1 Don't allow accounting export when the order totals changed + Xero fixes
  • Loading branch information
martijnvdbrug authored Aug 30, 2024
2 parents c878036 + 15e3ffe commit d6294cc
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 150 deletions.
6 changes: 6 additions & 0 deletions packages/vendure-plugin-invoices/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 4.1.0 (2024-08-30)

- Exporting credit invoices via accounting strategies now have their own method interface
- Don't allow accounting export when the order totals changed, to prevent mismatch between accounting export and invoice
- Added Due Date to Xero exports, which is needed for invoice approval

# 4.0.3 (2024-08-27)

- Try to find Xero contact by name first, then by email address
Expand Down
4 changes: 2 additions & 2 deletions packages/vendure-plugin-invoices/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vendure-hub/pinelab-invoice-plugin",
"version": "4.0.3",
"version": "4.1.0",
"description": "Vendure plugin for PDF invoice generation",
"author": "Martijn van de Brug <[email protected]>",
"homepage": "https://pinelab-plugins.com/",
Expand Down Expand Up @@ -33,6 +33,6 @@
},
"gitHead": "476f36da3aafea41fbf21c70774a30306f1d238f",
"devDependencies": {
"xero-node": "^9.0.0"
"xero-node": "9.2.0"
}
}
68 changes: 55 additions & 13 deletions packages/vendure-plugin-invoices/src/services/accounting.service.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import {
EntityRelationPaths,
ID,
JobQueue,
JobQueueService,
Logger,
Order,
OrderService,
Product,
RequestContext,
SerializedRequestContext,
TransactionalConnection,
UserInputError,
} from '@vendure/core';
import { loggerCtx, PLUGIN_INIT_OPTIONS } from '../constants';
import { AccountingExportStrategy } from '../strategies/accounting/accounting-export-strategy';
import { InvoicePluginConfig } from '../invoice.plugin';
import util from 'util';
import { InvoiceEntity } from '../entities/invoice.entity';
import { InvoicePluginConfig } from '../invoice.plugin';
import {
AccountingExportStrategy,
ExternalReference,
} from '../strategies/accounting/accounting-export-strategy';

@Injectable()
export class AccountingService implements OnModuleInit {
Expand Down Expand Up @@ -55,7 +55,7 @@ export class AccountingService implements OnModuleInit {
job.data.orderCode
).catch((error: Error) => {
Logger.warn(
`Failed to export invoice to accounting platform for '${job.data.orderCode}': ${error?.message}`,
`Failed to export invoice '${job.data.invoiceNumber}' to accounting platform for '${job.data.orderCode}': ${error?.message}`,
loggerCtx
);
throw error;
Expand Down Expand Up @@ -120,12 +120,27 @@ export class AccountingService implements OnModuleInit {
}
const invoiceRepository = this.connection.getRepository(ctx, InvoiceEntity);
try {
const reference = await strategy.exportInvoice(
ctx,
invoice,
order,
invoice.isCreditInvoiceFor
);
if (
!this.orderMatchesInvoice(order, invoice) &&
!invoice.isCreditInvoice
) {
// Throw an error when order totals don't match to prevent re-exporting wrong data.
// Credit invoices are allowed, because they use the reversed invoice.orderTotals instead of the order data itself
throw Error(
`Order '${order.code}' has changed compared to the invoice. Can not export this invoice again!`
);
}
let reference: ExternalReference;
if (invoice.isCreditInvoice) {
reference = await strategy.exportCreditInvoice(
ctx,
invoice,
invoice.isCreditInvoiceFor!, // this is always defined when it's a creditInvoice
order
);
} else {
reference = await strategy.exportInvoice(ctx, invoice, order);
}
await invoiceRepository.update(invoice.id, {
accountingReference: reference,
});
Expand Down Expand Up @@ -168,6 +183,33 @@ export class AccountingService implements OnModuleInit {
);
}

/**
* Checks if the total and tax rates of the order still match the ones from the invoice.
* When they differ, it means the order changed compared to the invoice.
*
* This should not be used with credit invoices, as their totals will mostly differ from the order,
* because a new invoice is created immediately
*/
private orderMatchesInvoice(order: Order, invoice: InvoiceEntity): boolean {
if (
order.total !== invoice.orderTotals.total ||
order.totalWithTax !== invoice.orderTotals.totalWithTax
) {
// Totals don't match anymore
return false;
}
// All order tax summaries should have a matching invoice tax summary
return order.taxSummary.every((orderSummary) => {
const matchingInvoiceSummary = invoice.orderTotals.taxSummaries.find(
(invoiceSummary) =>
invoiceSummary.taxRate === orderSummary.taxRate &&
invoiceSummary.taxBase === orderSummary.taxBase
);
// If no matching tax summary is found, the order doesn't match the invoice
return !!matchingInvoiceSummary;
});
}

private async getInvoiceByNumber(
ctx: RequestContext,
invoiceNumber: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,25 @@ export interface AccountingExportStrategy {
exportInvoice(
ctx: RequestContext,
invoice: InvoiceEntity,
order: Order,
order: Order
): Promise<ExternalReference> | ExternalReference;

/**
* Export the given Credit Invoice to the external accounting system.
* You should use invoice.orderTotals for the credit invoice and NOT the data from the order, because that
* will be the current (modified) order object, not the credit invoice.
*
* You can still use order.code and address details from the order object, just not the prices.
*
* This function will be executed asynchronously in via the JobQueue.
*/
exportCreditInvoice(
ctx: RequestContext,
invoice: InvoiceEntity,
/**
* If the invoice is a credit invoice, this will be the original invoice that the credit invoice is for.
* The original invoice that the credit invoice is for.
*/
isCreditInvoiceFor?: InvoiceEntity
isCreditInvoiceFor: InvoiceEntity,
order: Order
): Promise<ExternalReference> | ExternalReference;
}
Loading

0 comments on commit d6294cc

Please sign in to comment.