refactor: ♻️ add new error subclasses and use in tax-provider-utils

This commit is contained in:
Adrian Pilarczyk 2023-09-26 14:37:48 +02:00
parent 3c20d095ff
commit df4da32b4a
11 changed files with 62 additions and 41 deletions

6
apps/taxes/src/error.ts Normal file
View file

@ -0,0 +1,6 @@
import ModernError from "modern-errors";
import modernErrorsSerialize from "modern-errors-serialize";
export const BaseError = ModernError.subclass("BaseError", {
plugins: [modernErrorsSerialize],
});

View file

@ -1,14 +1,14 @@
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 { TaxUnexpectedError } from "../../taxes/tax-error";
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
import { avataxAddressFactory } from "../address-factory";
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
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 { 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 {
private matchDocumentType(config: AvataxConfig): DocumentType {
@ -31,7 +31,7 @@ export class AvataxCalculateTaxesPayloadTransformer {
return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.userEmail);
}
throw new Error("Cannot resolve customer code");
throw new TaxUnexpectedError("Cannot resolve customer code");
}
async transform(

View file

@ -14,11 +14,11 @@ export class AvataxCalculateTaxesResponseLinesTransformer {
return {
total_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
line.lineAmount,
new Error("line.lineAmount is undefined")
"line.lineAmount is undefined",
),
total_net_amount: taxProviderUtils.resolveOptionalOrThrow(
line.lineAmount,
new Error("line.lineAmount is undefined")
"line.lineAmount is undefined",
),
tax_rate: 0,
};
@ -26,21 +26,21 @@ export class AvataxCalculateTaxesResponseLinesTransformer {
const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
line.taxCalculated,
new Error("line.taxCalculated is undefined")
"line.taxCalculated is undefined",
);
const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow(
line.taxableAmount,
new Error("line.taxableAmount is undefined")
"line.taxableAmount is undefined",
);
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
lineTotalNetAmount + lineTaxCalculated
lineTotalNetAmount + lineTaxCalculated,
);
return {
total_gross_amount: lineTotalGrossAmount,
total_net_amount: lineTotalNetAmount,
/*
* avatax doesnt return combined tax rate
* avatax doesn't return combined tax rate
* // todo: calculate percentage tax rate
*/ tax_rate: 0,
};

View file

@ -6,7 +6,7 @@ import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
export class AvataxCalculateTaxesResponseShippingTransformer {
transform(
transaction: TransactionModel
transaction: TransactionModel,
): Pick<
CalculateTaxesResponse,
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
@ -25,14 +25,14 @@ export class AvataxCalculateTaxesResponseShippingTransformer {
return {
shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
shippingLine.lineAmount,
new Error("shippingLine.lineAmount is undefined")
"shippingLine.lineAmount is undefined",
),
shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow(
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
*/
shipping_tax_rate: 0,
@ -41,14 +41,14 @@ export class AvataxCalculateTaxesResponseShippingTransformer {
const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
shippingLine.taxCalculated,
new Error("shippingLine.taxCalculated is undefined")
"shippingLine.taxCalculated is undefined",
);
const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow(
shippingLine.taxableAmount,
new Error("shippingLine.taxableAmount is undefined")
"shippingLine.taxableAmount is undefined",
);
const shippingGrossAmount = numbers.roundFloatToTwoDecimals(
shippingTaxableAmount + shippingTaxCalculated
shippingTaxableAmount + shippingTaxCalculated,
);
return {

View file

@ -7,9 +7,7 @@ export class AvataxOrderConfirmedResponseTransformer {
return {
id: taxProviderUtils.resolveOptionalOrThrow(
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.",
),
};
}

View 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");

View file

@ -9,9 +9,7 @@ describe("taxProviderUtils", () => {
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined)).toThrowError();
});
it("throws a custom error if value is undefined", () => {
expect(() =>
taxProviderUtils.resolveOptionalOrThrow(undefined, new Error("test"))
).toThrowError("test");
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined, "test")).toThrowError("test");
}),
it("returns value if value is not undefined", () => {
expect(taxProviderUtils.resolveOptionalOrThrow("test")).toBe("test");

View file

@ -1,25 +1,34 @@
import { z } from "zod";
import { TaxUnexpectedError } from "./tax-error";
/*
* 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.
* 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) {
throw error
? error
: new Error("Could not resolve data. Value needed for further calculation is undefined.");
throw new TaxUnexpectedError(
errorMessage
? errorMessage
: "Could not resolve data. Value needed for further calculation is undefined.",
);
}
return value;
}
function resolveStringOrThrow(value: string | undefined | null): string {
return z
const parseResult = z
.string({ required_error: "This field must be defined." })
.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 = {

View file

@ -13,14 +13,14 @@ import {
*/
export function matchPayloadLinesToResponseLines(
payloadLines: TaxBaseFragment["lines"],
responseLines: NonNullable<Breakdown["line_items"]>
responseLines: NonNullable<Breakdown["line_items"]>,
) {
return payloadLines.map((payloadLine) => {
const responseLine = responseLines.find((line) => line.id === payloadLine.sourceLine.id);
if (!responseLine) {
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 {
transform(
payload: TaxJarCalculateTaxesPayload,
response: TaxForOrderRes
response: TaxForOrderRes,
): TaxJarCalculateTaxesResponse["lines"] {
const responseLines = response.tax.breakdown?.line_items ?? [];
@ -40,15 +40,15 @@ export class TaxJarCalculateTaxesResponseLinesTransformer {
return lines.map((line) => {
const taxableAmount = taxProviderUtils.resolveOptionalOrThrow(
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(
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(
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 {

View file

@ -15,14 +15,14 @@ export function sumPayloadLines(lines: LineItem[]): number {
prev +
taxProviderUtils.resolveOptionalOrThrow(
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(
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(
order: OrderConfirmedSubscriptionFragment,
taxJarConfig: TaxJarConfig,
matches: TaxJarTaxCodeMatches
matches: TaxJarTaxCodeMatches,
): TaxJarOrderConfirmedTarget {
const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
const lineItems = linesTransformer.transform(order.lines, matches);

View file

@ -1,5 +1,6 @@
{
"words": [
"Autocommit",
"Adyen",
"Afterpay",
"Algolia",