refactor: ♻️ add new error subclasses and use in tax-provider-utils
This commit is contained in:
parent
3c20d095ff
commit
df4da32b4a
11 changed files with 62 additions and 41 deletions
6
apps/taxes/src/error.ts
Normal file
6
apps/taxes/src/error.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import ModernError from "modern-errors";
|
||||||
|
import modernErrorsSerialize from "modern-errors-serialize";
|
||||||
|
|
||||||
|
export const BaseError = ModernError.subclass("BaseError", {
|
||||||
|
plugins: [modernErrorsSerialize],
|
||||||
|
});
|
|
@ -1,14 +1,14 @@
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
|
import { TaxUnexpectedError } from "../../taxes/tax-error";
|
||||||
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
import { avataxAddressFactory } from "../address-factory";
|
||||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||||
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
||||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer";
|
import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer";
|
||||||
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
|
||||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
|
||||||
|
|
||||||
export class AvataxCalculateTaxesPayloadTransformer {
|
export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
private matchDocumentType(config: AvataxConfig): DocumentType {
|
||||||
|
@ -31,7 +31,7 @@ export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.userEmail);
|
return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.userEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Cannot resolve customer code");
|
throw new TaxUnexpectedError("Cannot resolve customer code");
|
||||||
}
|
}
|
||||||
|
|
||||||
async transform(
|
async transform(
|
||||||
|
|
|
@ -14,11 +14,11 @@ export class AvataxCalculateTaxesResponseLinesTransformer {
|
||||||
return {
|
return {
|
||||||
total_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
|
total_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line.lineAmount,
|
line.lineAmount,
|
||||||
new Error("line.lineAmount is undefined")
|
"line.lineAmount is undefined",
|
||||||
),
|
),
|
||||||
total_net_amount: taxProviderUtils.resolveOptionalOrThrow(
|
total_net_amount: taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line.lineAmount,
|
line.lineAmount,
|
||||||
new Error("line.lineAmount is undefined")
|
"line.lineAmount is undefined",
|
||||||
),
|
),
|
||||||
tax_rate: 0,
|
tax_rate: 0,
|
||||||
};
|
};
|
||||||
|
@ -26,21 +26,21 @@ export class AvataxCalculateTaxesResponseLinesTransformer {
|
||||||
|
|
||||||
const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
|
const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line.taxCalculated,
|
line.taxCalculated,
|
||||||
new Error("line.taxCalculated is undefined")
|
"line.taxCalculated is undefined",
|
||||||
);
|
);
|
||||||
const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow(
|
const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line.taxableAmount,
|
line.taxableAmount,
|
||||||
new Error("line.taxableAmount is undefined")
|
"line.taxableAmount is undefined",
|
||||||
);
|
);
|
||||||
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
|
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
|
||||||
lineTotalNetAmount + lineTaxCalculated
|
lineTotalNetAmount + lineTaxCalculated,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total_gross_amount: lineTotalGrossAmount,
|
total_gross_amount: lineTotalGrossAmount,
|
||||||
total_net_amount: lineTotalNetAmount,
|
total_net_amount: lineTotalNetAmount,
|
||||||
/*
|
/*
|
||||||
* avatax doesnt return combined tax rate
|
* avatax doesn't return combined tax rate
|
||||||
* // todo: calculate percentage tax rate
|
* // todo: calculate percentage tax rate
|
||||||
*/ tax_rate: 0,
|
*/ tax_rate: 0,
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesResponseShippingTransformer {
|
export class AvataxCalculateTaxesResponseShippingTransformer {
|
||||||
transform(
|
transform(
|
||||||
transaction: TransactionModel
|
transaction: TransactionModel,
|
||||||
): Pick<
|
): Pick<
|
||||||
CalculateTaxesResponse,
|
CalculateTaxesResponse,
|
||||||
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
|
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
|
||||||
|
@ -25,14 +25,14 @@ export class AvataxCalculateTaxesResponseShippingTransformer {
|
||||||
return {
|
return {
|
||||||
shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
|
shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
|
||||||
shippingLine.lineAmount,
|
shippingLine.lineAmount,
|
||||||
new Error("shippingLine.lineAmount is undefined")
|
"shippingLine.lineAmount is undefined",
|
||||||
),
|
),
|
||||||
shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow(
|
shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow(
|
||||||
shippingLine.lineAmount,
|
shippingLine.lineAmount,
|
||||||
new Error("shippingLine.lineAmount is undefined")
|
"shippingLine.lineAmount is undefined",
|
||||||
),
|
),
|
||||||
/*
|
/*
|
||||||
* avatax doesnt return combined tax rate
|
* avatax doesn't return combined tax rate
|
||||||
* // todo: calculate percentage tax rate
|
* // todo: calculate percentage tax rate
|
||||||
*/
|
*/
|
||||||
shipping_tax_rate: 0,
|
shipping_tax_rate: 0,
|
||||||
|
@ -41,14 +41,14 @@ export class AvataxCalculateTaxesResponseShippingTransformer {
|
||||||
|
|
||||||
const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
|
const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
shippingLine.taxCalculated,
|
shippingLine.taxCalculated,
|
||||||
new Error("shippingLine.taxCalculated is undefined")
|
"shippingLine.taxCalculated is undefined",
|
||||||
);
|
);
|
||||||
const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow(
|
const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
shippingLine.taxableAmount,
|
shippingLine.taxableAmount,
|
||||||
new Error("shippingLine.taxableAmount is undefined")
|
"shippingLine.taxableAmount is undefined",
|
||||||
);
|
);
|
||||||
const shippingGrossAmount = numbers.roundFloatToTwoDecimals(
|
const shippingGrossAmount = numbers.roundFloatToTwoDecimals(
|
||||||
shippingTaxableAmount + shippingTaxCalculated
|
shippingTaxableAmount + shippingTaxCalculated,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -7,9 +7,7 @@ export class AvataxOrderConfirmedResponseTransformer {
|
||||||
return {
|
return {
|
||||||
id: taxProviderUtils.resolveOptionalOrThrow(
|
id: taxProviderUtils.resolveOptionalOrThrow(
|
||||||
response.code,
|
response.code,
|
||||||
new Error(
|
"Could not update the order metadata with AvaTax transaction code because it was not returned from the createTransaction mutation.",
|
||||||
"Could not update the order metadata with AvaTax transaction code because it was not returned from the createTransaction mutation."
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
9
apps/taxes/src/modules/taxes/tax-error.ts
Normal file
9
apps/taxes/src/modules/taxes/tax-error.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { BaseError } from "../../error";
|
||||||
|
|
||||||
|
const TaxError = BaseError.subclass("TaxError");
|
||||||
|
|
||||||
|
// Errors that shouldn't happen
|
||||||
|
export const TaxUnexpectedError = TaxError.subclass("TaxUnexpectedError");
|
||||||
|
|
||||||
|
// Errors that are expected to happen
|
||||||
|
export const TaxExpectedError = TaxError.subclass("TaxExpectedError");
|
|
@ -9,9 +9,7 @@ describe("taxProviderUtils", () => {
|
||||||
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined)).toThrowError();
|
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined)).toThrowError();
|
||||||
});
|
});
|
||||||
it("throws a custom error if value is undefined", () => {
|
it("throws a custom error if value is undefined", () => {
|
||||||
expect(() =>
|
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined, "test")).toThrowError("test");
|
||||||
taxProviderUtils.resolveOptionalOrThrow(undefined, new Error("test"))
|
|
||||||
).toThrowError("test");
|
|
||||||
}),
|
}),
|
||||||
it("returns value if value is not undefined", () => {
|
it("returns value if value is not undefined", () => {
|
||||||
expect(taxProviderUtils.resolveOptionalOrThrow("test")).toBe("test");
|
expect(taxProviderUtils.resolveOptionalOrThrow("test")).toBe("test");
|
||||||
|
|
|
@ -1,25 +1,34 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { TaxUnexpectedError } from "./tax-error";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The providers sdk types claim to sometimes return undefined.
|
* The providers sdk types claim to sometimes return undefined.
|
||||||
* If it ever happens, we have nothing to fall back to, so we throw an error.
|
* If it ever happens, we have nothing to fall back to, so we throw an error.
|
||||||
* Should only be used for values that are required for further calculation.
|
* Should only be used for values that are required for further calculation.
|
||||||
*/
|
*/
|
||||||
function resolveOptionalOrThrow<T>(value: T | undefined | null, error?: Error): T {
|
function resolveOptionalOrThrow<T>(value: T | undefined | null, errorMessage?: string): T {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
throw error
|
throw new TaxUnexpectedError(
|
||||||
? error
|
errorMessage
|
||||||
: new Error("Could not resolve data. Value needed for further calculation is undefined.");
|
? errorMessage
|
||||||
|
: "Could not resolve data. Value needed for further calculation is undefined.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveStringOrThrow(value: string | undefined | null): string {
|
function resolveStringOrThrow(value: string | undefined | null): string {
|
||||||
return z
|
const parseResult = z
|
||||||
.string({ required_error: "This field must be defined." })
|
.string({ required_error: "This field must be defined." })
|
||||||
.min(1, { message: "This field can not be empty." })
|
.min(1, { message: "This field can not be empty." })
|
||||||
.parse(value);
|
.safeParse(value);
|
||||||
|
|
||||||
|
if (!parseResult.success) {
|
||||||
|
throw new TaxUnexpectedError(parseResult.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const taxProviderUtils = {
|
export const taxProviderUtils = {
|
||||||
|
|
|
@ -13,14 +13,14 @@ import {
|
||||||
*/
|
*/
|
||||||
export function matchPayloadLinesToResponseLines(
|
export function matchPayloadLinesToResponseLines(
|
||||||
payloadLines: TaxBaseFragment["lines"],
|
payloadLines: TaxBaseFragment["lines"],
|
||||||
responseLines: NonNullable<Breakdown["line_items"]>
|
responseLines: NonNullable<Breakdown["line_items"]>,
|
||||||
) {
|
) {
|
||||||
return payloadLines.map((payloadLine) => {
|
return payloadLines.map((payloadLine) => {
|
||||||
const responseLine = responseLines.find((line) => line.id === payloadLine.sourceLine.id);
|
const responseLine = responseLines.find((line) => line.id === payloadLine.sourceLine.id);
|
||||||
|
|
||||||
if (!responseLine) {
|
if (!responseLine) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Saleor product line with id ${payloadLine.sourceLine.id} not found in TaxJar response.`
|
`Saleor product line with id ${payloadLine.sourceLine.id} not found in TaxJar response.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ export function matchPayloadLinesToResponseLines(
|
||||||
export class TaxJarCalculateTaxesResponseLinesTransformer {
|
export class TaxJarCalculateTaxesResponseLinesTransformer {
|
||||||
transform(
|
transform(
|
||||||
payload: TaxJarCalculateTaxesPayload,
|
payload: TaxJarCalculateTaxesPayload,
|
||||||
response: TaxForOrderRes
|
response: TaxForOrderRes,
|
||||||
): TaxJarCalculateTaxesResponse["lines"] {
|
): TaxJarCalculateTaxesResponse["lines"] {
|
||||||
const responseLines = response.tax.breakdown?.line_items ?? [];
|
const responseLines = response.tax.breakdown?.line_items ?? [];
|
||||||
|
|
||||||
|
@ -40,15 +40,15 @@ export class TaxJarCalculateTaxesResponseLinesTransformer {
|
||||||
return lines.map((line) => {
|
return lines.map((line) => {
|
||||||
const taxableAmount = taxProviderUtils.resolveOptionalOrThrow(
|
const taxableAmount = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line?.taxable_amount,
|
line?.taxable_amount,
|
||||||
new Error("Line taxable amount is required to calculate net amount")
|
"Line taxable amount is required to calculate net amount",
|
||||||
);
|
);
|
||||||
const taxCollectable = taxProviderUtils.resolveOptionalOrThrow(
|
const taxCollectable = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line?.tax_collectable,
|
line?.tax_collectable,
|
||||||
new Error("Line tax collectable is required to calculate net amount")
|
"Line tax collectable is required to calculate net amount",
|
||||||
);
|
);
|
||||||
const taxRate = taxProviderUtils.resolveOptionalOrThrow(
|
const taxRate = taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line?.combined_tax_rate,
|
line?.combined_tax_rate,
|
||||||
new Error("Line combined tax rate is required to calculate net amount")
|
"Line combined tax rate is required to calculate net amount",
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -15,14 +15,14 @@ export function sumPayloadLines(lines: LineItem[]): number {
|
||||||
prev +
|
prev +
|
||||||
taxProviderUtils.resolveOptionalOrThrow(
|
taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line.unit_price,
|
line.unit_price,
|
||||||
new Error("Line unit_price is required to calculate order taxes")
|
"Line unit_price is required to calculate order taxes",
|
||||||
) *
|
) *
|
||||||
taxProviderUtils.resolveOptionalOrThrow(
|
taxProviderUtils.resolveOptionalOrThrow(
|
||||||
line.quantity,
|
line.quantity,
|
||||||
new Error("Line quantity is required to calculate order taxes")
|
"Line quantity is required to calculate order taxes",
|
||||||
),
|
),
|
||||||
0
|
0,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export class TaxJarOrderConfirmedPayloadTransformer {
|
||||||
transform(
|
transform(
|
||||||
order: OrderConfirmedSubscriptionFragment,
|
order: OrderConfirmedSubscriptionFragment,
|
||||||
taxJarConfig: TaxJarConfig,
|
taxJarConfig: TaxJarConfig,
|
||||||
matches: TaxJarTaxCodeMatches
|
matches: TaxJarTaxCodeMatches,
|
||||||
): TaxJarOrderConfirmedTarget {
|
): TaxJarOrderConfirmedTarget {
|
||||||
const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
|
const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
|
||||||
const lineItems = linesTransformer.transform(order.lines, matches);
|
const lineItems = linesTransformer.transform(order.lines, matches);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"words": [
|
"words": [
|
||||||
|
"Autocommit",
|
||||||
"Adyen",
|
"Adyen",
|
||||||
"Afterpay",
|
"Afterpay",
|
||||||
"Algolia",
|
"Algolia",
|
||||||
|
|
Loading…
Reference in a new issue