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 { 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(

View file

@ -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,
}; };

View file

@ -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 {

View file

@ -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."
)
), ),
}; };
} }

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

View file

@ -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 = {

View file

@ -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 {

View file

@ -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);

View file

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