diff --git a/.changeset/chilled-rats-love.md b/.changeset/chilled-rats-love.md new file mode 100644 index 0000000..138c632 --- /dev/null +++ b/.changeset/chilled-rats-love.md @@ -0,0 +1,5 @@ +--- +"saleor-app-taxes": minor +--- + +Adds new way of distributing discounts on items (proportional). Adds distinguishment between when TaxJar nexus was found and was not. Now, the "not found" behavior is not treated as error, and will return untaxed values. Fixes bugs: item quantity in TaxJar; when shipping = 0; pricesEnteredWithTax influences shipping price. diff --git a/apps/taxes/src/mocks.ts b/apps/taxes/src/mocks.ts new file mode 100644 index 0000000..13ff847 --- /dev/null +++ b/apps/taxes/src/mocks.ts @@ -0,0 +1,109 @@ +import { OrderCreatedSubscriptionFragment, OrderStatus } from "../generated/graphql"; + +export const defaultOrder: OrderCreatedSubscriptionFragment = { + id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2", + user: { + id: "VXNlcjoyMDg0NTEwNDEw", + email: "happy.customer@saleor.io", + }, + created: "2023-05-25T09:18:55.203440+00:00", + status: OrderStatus.Unfulfilled, + channel: { + id: "Q2hhbm5lbDox", + slug: "default-channel", + }, + shippingAddress: { + streetAddress1: "600 Montgomery St", + streetAddress2: "", + city: "SAN FRANCISCO", + countryArea: "CA", + postalCode: "94111", + country: { + code: "US", + }, + }, + billingAddress: { + streetAddress1: "600 Montgomery St", + streetAddress2: "", + city: "SAN FRANCISCO", + countryArea: "CA", + postalCode: "94111", + country: { + code: "US", + }, + }, + total: { + currency: "USD", + net: { + amount: 239.17, + }, + tax: { + amount: 15.54, + }, + }, + shippingPrice: { + gross: { + amount: 59.17, + }, + net: { + amount: 59.17, + }, + }, + lines: [ + { + productSku: "328223580", + productName: "Monospace Tee", + quantity: 3, + unitPrice: { + net: { + amount: 20, + }, + }, + totalPrice: { + net: { + amount: 60, + }, + tax: { + amount: 5.18, + }, + }, + }, + { + productSku: "328223581", + productName: "Monospace Tee", + quantity: 1, + unitPrice: { + net: { + amount: 20, + }, + }, + totalPrice: { + net: { + amount: 20, + }, + tax: { + amount: 1.73, + }, + }, + }, + { + productSku: "118223581", + productName: "Paul's Balance 420", + quantity: 2, + unitPrice: { + net: { + amount: 50, + }, + }, + totalPrice: { + net: { + amount: 100, + }, + tax: { + amount: 8.63, + }, + }, + }, + ], + discounts: [], +}; diff --git a/apps/taxes/src/modules/app/get-app-config.test.ts b/apps/taxes/src/modules/app/get-app-config.test.ts index 0dad227..70edbb6 100644 --- a/apps/taxes/src/modules/app/get-app-config.test.ts +++ b/apps/taxes/src/modules/app/get-app-config.test.ts @@ -62,14 +62,14 @@ const mockedMetadata: MetadataItem[] = [ vi.stubEnv("SECRET_KEY", mockedSecretKey); describe("getAppConfig", () => { - it("should return empty providers and channels config when no metadata", () => { + it("returns empty providers and channels config when no metadata", () => { const { providers, channels } = getAppConfig([]); expect(providers).toEqual([]); expect(channels).toEqual({}); }); - it("should return decrypted providers and channels config when metadata provided", () => { + it("returns decrypted providers and channels config when metadata provided", () => { const { providers, channels } = getAppConfig(mockedMetadata); expect(providers).toEqual(mockedProviders); diff --git a/apps/taxes/src/modules/app/webhook-response.test.ts b/apps/taxes/src/modules/app/webhook-response.test.ts new file mode 100644 index 0000000..0e4f352 --- /dev/null +++ b/apps/taxes/src/modules/app/webhook-response.test.ts @@ -0,0 +1,39 @@ +import { NextApiResponse } from "next"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { WebhookResponse } from "./webhook-response"; + +let jsonMock = vi.fn(); +let statusMock = vi.fn().mockReturnValueOnce({ json: jsonMock }); + +let mockResponse = { + status: statusMock, +} as unknown as NextApiResponse; + +beforeEach(() => { + jsonMock = vi.fn(); + statusMock = vi.fn().mockReturnValueOnce({ json: jsonMock }); + + mockResponse = { + status: statusMock, + } as unknown as NextApiResponse; +}); + +describe("WebhookResponse", () => { + it("returns 500 when thrown unexpected error", () => { + const webhookResponse = new WebhookResponse(mockResponse); + const unexpectedError = new Error("Unexpected error"); + + webhookResponse.error(unexpectedError); + + expect(statusMock).toHaveBeenCalledWith(500); + }); + + it("returns 200 and data when success is called", () => { + const webhookResponse = new WebhookResponse(mockResponse); + + webhookResponse.success({ foo: "bar" }); + + expect(statusMock).toHaveBeenCalledWith(200); + expect(jsonMock).toHaveBeenCalledWith({ foo: "bar" }); + }); +}); diff --git a/apps/taxes/src/modules/app/webhook-response.ts b/apps/taxes/src/modules/app/webhook-response.ts index be0f381..d7bd56d 100644 --- a/apps/taxes/src/modules/app/webhook-response.ts +++ b/apps/taxes/src/modules/app/webhook-response.ts @@ -2,24 +2,35 @@ import { NextApiResponse } from "next"; import { createLogger, Logger } from "../../lib/logger"; -/* - * idea: distinguish between async and sync webhooks - * when sync webhooks, require passing the event and enforce the required response format using ctx.buildResponse - * when async webhooks, dont require anything - */ export class WebhookResponse { private logger: Logger; constructor(private res: NextApiResponse) { this.logger = createLogger({ event: "WebhookResponse" }); } - failure(error: string) { - this.logger.debug({ error }, "failure called with:"); - return this.res.status(500).json({ error }); + private returnSuccess(data?: unknown) { + this.logger.debug({ data }, "success called with:"); + return this.res.status(200).json(data ?? {}); } - success(data?: any) { - this.logger.debug({ data }, "success called with:"); - return this.res.send(data); + private returnError(errorMessage: string) { + this.logger.debug({ errorMessage }, "returning error:"); + return this.res.status(500).json({ error: errorMessage }); + } + + private resolveError(error: unknown) { + if (error instanceof Error) { + this.logger.error(error.stack, "Unexpected error caught:"); + return this.returnError(error.message); + } + return this.returnError("Internal server error"); + } + + error(error: unknown) { + return this.resolveError(error); + } + + success(data?: unknown) { + return this.returnSuccess(data); } } diff --git a/apps/taxes/src/modules/avatax/maps/address-factory.test.ts b/apps/taxes/src/modules/avatax/address-factory.test.ts similarity index 100% rename from apps/taxes/src/modules/avatax/maps/address-factory.test.ts rename to apps/taxes/src/modules/avatax/address-factory.test.ts diff --git a/apps/taxes/src/modules/avatax/maps/address-factory.ts b/apps/taxes/src/modules/avatax/address-factory.ts similarity index 82% rename from apps/taxes/src/modules/avatax/maps/address-factory.ts rename to apps/taxes/src/modules/avatax/address-factory.ts index 2a55cd5..74ff9d9 100644 --- a/apps/taxes/src/modules/avatax/maps/address-factory.ts +++ b/apps/taxes/src/modules/avatax/address-factory.ts @@ -1,6 +1,6 @@ import { AddressLocationInfo as AvataxAddress } from "avatax/lib/models/AddressLocationInfo"; -import { ChannelAddress } from "../../channels-configuration/channels-config"; -import { AddressFragment as SaleorAddress } from "../../../../generated/graphql"; +import { ChannelAddress } from "../channels-configuration/channels-config"; +import { AddressFragment as SaleorAddress } from "../../../generated/graphql"; function mapSaleorAddressToAvataxAddress(address: SaleorAddress): AvataxAddress { return { diff --git a/apps/taxes/src/modules/avatax/avatax-client.ts b/apps/taxes/src/modules/avatax/avatax-client.ts index 573a82e..95cead0 100644 --- a/apps/taxes/src/modules/avatax/avatax-client.ts +++ b/apps/taxes/src/modules/avatax/avatax-client.ts @@ -72,19 +72,19 @@ export class AvataxClient { } async createTransaction({ model }: CreateTransactionArgs) { - this.logger.debug({ model }, "createTransaction called with:"); + this.logger.trace({ model }, "createTransaction called with:"); return this.client.createTransaction({ model }); } async commitTransaction(args: CommitTransactionArgs) { - this.logger.debug(args, "commitTransaction called with:"); + this.logger.trace(args, "commitTransaction called with:"); return this.client.commitTransaction(args); } async ping() { - this.logger.debug("ping called"); + this.logger.trace("ping called"); try { const result = await this.client.ping(); @@ -103,7 +103,7 @@ export class AvataxClient { } async validateAddress({ address }: ValidateAddressArgs) { - this.logger.debug({ address }, "validateAddress called with:"); + this.logger.trace({ address }, "validateAddress called with:"); return this.client.resolveAddress(address); } diff --git a/apps/taxes/src/modules/avatax/avatax-mock-factory.ts b/apps/taxes/src/modules/avatax/avatax-mock-factory.ts new file mode 100644 index 0000000..92329e2 --- /dev/null +++ b/apps/taxes/src/modules/avatax/avatax-mock-factory.ts @@ -0,0 +1,5 @@ +import { avataxMockTransactionFactory } from "./avatax-mock-transaction-factory"; + +export const avataxMockFactory = { + ...avataxMockTransactionFactory, +}; diff --git a/apps/taxes/src/modules/avatax/maps/mocks.ts b/apps/taxes/src/modules/avatax/avatax-mock-transaction-factory.ts similarity index 59% rename from apps/taxes/src/modules/avatax/maps/mocks.ts rename to apps/taxes/src/modules/avatax/avatax-mock-transaction-factory.ts index fdde194..535a99d 100644 --- a/apps/taxes/src/modules/avatax/maps/mocks.ts +++ b/apps/taxes/src/modules/avatax/avatax-mock-transaction-factory.ts @@ -8,44 +8,14 @@ import { JurisdictionType } from "avatax/lib/enums/JurisdictionType"; import { LiabilityType } from "avatax/lib/enums/LiabilityType"; import { RateType } from "avatax/lib/enums/RateType"; import { TransactionModel } from "avatax/lib/models/TransactionModel"; -import { AvataxCalculateTaxesMapPayloadArgs } from "./avatax-calculate-taxes-map"; -const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { - id: 0, - code: "30ddd987-ed4a-49f1-bba1-ec555e417511", - companyId: 7799660, - date: new Date(), - paymentDate: new Date(), - status: DocumentStatus.Temporary, - type: DocumentType.SalesOrder, - batchCode: "", - currencyCode: "USD", - exchangeRateCurrencyCode: "USD", - customerUsageType: "", - entityUseCode: "", - customerVendorCode: "VXNlcjoyMDg0NTEwNDEw", - customerCode: "VXNlcjoyMDg0NTEwNDEw", - exemptNo: "", - reconciled: false, - locationCode: "", - reportingLocationCode: "", - purchaseOrderNo: "", - referenceCode: "", - salespersonCode: "", - totalAmount: 199.17, +const taxExcludedNoShipping: Partial = { + totalAmount: 80, totalExempt: 0, totalDiscount: 0, - totalTax: 18.92, - totalTaxable: 199.17, - totalTaxCalculated: 18.92, - adjustmentReason: AdjustmentReason.NotAdjusted, - locked: false, - version: 1, - exchangeRateEffectiveDate: new Date(), - exchangeRate: 1, - modifiedDate: new Date(), - modifiedUserId: 6479978, - taxDate: new Date(), + totalTax: 7.6, + totalTaxable: 80, + totalTaxCalculated: 7.6, lines: [ { id: 0, @@ -59,17 +29,17 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { exemptNo: "", isItemTaxable: true, itemCode: "", - lineAmount: 80, - quantity: 4, + lineAmount: 40, + quantity: 2, ref1: "", ref2: "", - reportingDate: new Date(), - tax: 7.6, - taxableAmount: 80, - taxCalculated: 7.6, + reportingDate: new Date(new Date("2023-05-23")), + tax: 3.8, + taxableAmount: 40, + taxCalculated: 3.8, taxCode: "P0000000", taxCodeId: 8087, - taxDate: new Date(), + taxDate: new Date(new Date("2023-05-23")), taxIncluded: false, details: [ { @@ -86,23 +56,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.State, nonTaxableAmount: 0, rate: 0.06, - tax: 4.8, - taxableAmount: 80, + tax: 2.4, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA STATE TAX", taxAuthorityTypeId: 45, - taxCalculated: 4.8, + taxCalculated: 2.4, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 80, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 4.8, - reportingTaxCalculated: 4.8, + reportingTax: 2.4, + reportingTaxCalculated: 2.4, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -120,23 +90,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.County, nonTaxableAmount: 0, rate: 0.0025, - tax: 0.2, - taxableAmount: 80, + tax: 0.1, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA COUNTY TAX", taxAuthorityTypeId: 45, - taxCalculated: 0.2, + taxCalculated: 0.1, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 80, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 0.2, - reportingTaxCalculated: 0.2, + reportingTax: 0.1, + reportingTaxCalculated: 0.1, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -154,23 +124,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.Special, nonTaxableAmount: 0, rate: 0.0225, - tax: 1.8, - taxableAmount: 80, + tax: 0.9, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA SPECIAL TAX", taxAuthorityTypeId: 45, - taxCalculated: 1.8, + taxCalculated: 0.9, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 80, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 1.8, - reportingTaxCalculated: 1.8, + reportingTax: 0.9, + reportingTaxCalculated: 0.9, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -188,23 +158,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.Special, nonTaxableAmount: 0, rate: 0.01, - tax: 0.8, - taxableAmount: 80, + tax: 0.4, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA SPECIAL TAX", taxAuthorityTypeId: 45, - taxCalculated: 0.8, + taxCalculated: 0.4, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 80, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 0.8, - reportingTaxCalculated: 0.8, + reportingTax: 0.4, + reportingTaxCalculated: 0.4, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -227,17 +197,17 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { exemptNo: "", isItemTaxable: true, itemCode: "", - lineAmount: 60, - quantity: 3, + lineAmount: 40, + quantity: 2, ref1: "", ref2: "", - reportingDate: new Date(), - tax: 5.7, - taxableAmount: 60, - taxCalculated: 5.7, + reportingDate: new Date(new Date("2023-05-23")), + tax: 3.8, + taxableAmount: 40, + taxCalculated: 3.8, taxCode: "P0000000", taxCodeId: 8087, - taxDate: new Date(), + taxDate: new Date(new Date("2023-05-23")), taxIncluded: false, details: [ { @@ -254,23 +224,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.State, nonTaxableAmount: 0, rate: 0.06, - tax: 3.6, - taxableAmount: 60, + tax: 2.4, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA STATE TAX", taxAuthorityTypeId: 45, - taxCalculated: 3.6, + taxCalculated: 2.4, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 60, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 3.6, - reportingTaxCalculated: 3.6, + reportingTax: 2.4, + reportingTaxCalculated: 2.4, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -288,23 +258,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.County, nonTaxableAmount: 0, rate: 0.0025, - tax: 0.15, - taxableAmount: 60, + tax: 0.1, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA COUNTY TAX", taxAuthorityTypeId: 45, - taxCalculated: 0.15, + taxCalculated: 0.1, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 60, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 0.15, - reportingTaxCalculated: 0.15, + reportingTax: 0.1, + reportingTaxCalculated: 0.1, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -322,23 +292,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.Special, nonTaxableAmount: 0, rate: 0.0225, - tax: 1.35, - taxableAmount: 60, + tax: 0.9, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA SPECIAL TAX", taxAuthorityTypeId: 45, - taxCalculated: 1.35, + taxCalculated: 0.9, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 60, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 1.35, - reportingTaxCalculated: 1.35, + reportingTax: 0.9, + reportingTaxCalculated: 0.9, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -356,23 +326,23 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { jurisdictionType: JurisdictionType.Special, nonTaxableAmount: 0, rate: 0.01, - tax: 0.6, - taxableAmount: 60, + tax: 0.4, + taxableAmount: 40, taxType: "Use", taxSubTypeId: "U", taxName: "CA SPECIAL TAX", taxAuthorityTypeId: 45, - taxCalculated: 0.6, + taxCalculated: 0.4, rateType: RateType.General, rateTypeCode: "G", unitOfBasis: "PerCurrencyUnit", isNonPassThru: false, isFee: false, - reportingTaxableUnits: 60, + reportingTaxableUnits: 40, reportingNonTaxableUnits: 0, reportingExemptUnits: 0, - reportingTax: 0.6, - reportingTaxCalculated: 0.6, + reportingTax: 0.4, + reportingTaxCalculated: 0.4, liabilityType: LiabilityType.Seller, chargedTo: ChargedTo.Buyer, }, @@ -384,38 +354,6 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { vatNumberTypeId: 0, }, ], - addresses: [ - { - id: 0, - transactionId: 0, - boundaryLevel: BoundaryLevel.Zip5, - line1: "123 Palm Grove Ln", - line2: "", - line3: "", - city: "LOS ANGELES", - region: "CA", - postalCode: "90002", - country: "US", - taxRegionId: 4017056, - latitude: "33.948712", - longitude: "-118.245951", - }, - { - id: 0, - transactionId: 0, - boundaryLevel: BoundaryLevel.Zip5, - line1: "8559 Lake Avenue", - line2: "", - line3: "", - city: "New York", - region: "NY", - postalCode: "10001", - country: "US", - taxRegionId: 2088629, - latitude: "40.748481", - longitude: "-73.993125", - }, - ], summary: [ { country: "US", @@ -429,10 +367,10 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA STATE TAX", rateType: RateType.General, - taxable: 199.17, + taxable: 80, rate: 0.06, - tax: 11.95, - taxCalculated: 11.95, + tax: 4.8, + taxCalculated: 4.8, nonTaxable: 0, exemption: 0, }, @@ -448,10 +386,10 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA COUNTY TAX", rateType: RateType.General, - taxable: 199.17, + taxable: 80, rate: 0.0025, - tax: 0.5, - taxCalculated: 0.5, + tax: 0.2, + taxCalculated: 0.2, nonTaxable: 0, exemption: 0, }, @@ -467,10 +405,10 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA SPECIAL TAX", rateType: RateType.General, - taxable: 199.17, + taxable: 80, rate: 0.01, - tax: 1.99, - taxCalculated: 1.99, + tax: 0.8, + taxCalculated: 0.8, nonTaxable: 0, exemption: 0, }, @@ -486,52 +424,616 @@ const NO_SHIPPING_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA SPECIAL TAX", rateType: RateType.General, - taxable: 199.17, + taxable: 80, rate: 0.0225, - tax: 4.48, - taxCalculated: 4.48, + tax: 1.8, + taxCalculated: 1.8, nonTaxable: 0, exemption: 0, }, ], }; -const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { - id: 0, - code: "8fc875ce-a929-4556-9f30-0165b1597d9f", - companyId: 7799640, - date: new Date(), - paymentDate: new Date(), - status: DocumentStatus.Temporary, - type: DocumentType.SalesOrder, - batchCode: "", - currencyCode: "USD", - exchangeRateCurrencyCode: "USD", - customerUsageType: "", - entityUseCode: "", - customerVendorCode: "VXNlcjoyMDg0NTEwNDEw", - customerCode: "VXNlcjoyMDg0NTEwNDEw", - exemptNo: "", - reconciled: false, - locationCode: "", - reportingLocationCode: "", - purchaseOrderNo: "", - referenceCode: "", - salespersonCode: "", - totalAmount: 107.31, +const taxExcludedShipping: Partial = { + totalAmount: 157.51, totalExempt: 0, totalDiscount: 0, - totalTax: 10.2, - totalTaxable: 107.31, - totalTaxCalculated: 10.2, - adjustmentReason: AdjustmentReason.NotAdjusted, - locked: false, - version: 1, - exchangeRateEffectiveDate: new Date(), - exchangeRate: 1, - modifiedDate: new Date(), - modifiedUserId: 6479978, - taxDate: new Date(), + totalTax: 14.96, + totalTaxable: 157.51, + totalTaxCalculated: 14.96, + lines: [ + { + id: 0, + transactionId: 0, + lineNumber: "1", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 40, + quantity: 2, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-23"), + tax: 3.8, + taxableAmount: 40, + taxCalculated: 3.8, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-23"), + taxIncluded: false, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.4, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.4, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.4, + reportingTaxCalculated: 2.4, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.1, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.1, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.1, + reportingTaxCalculated: 0.1, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 0.9, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.9, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.9, + reportingTaxCalculated: 0.9, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.4, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.4, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.4, + reportingTaxCalculated: 0.4, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "2", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 40, + quantity: 2, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-23"), + tax: 3.8, + taxableAmount: 40, + taxCalculated: 3.8, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-23"), + taxIncluded: false, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.4, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.4, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.4, + reportingTaxCalculated: 2.4, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.1, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.1, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.1, + reportingTaxCalculated: 0.1, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 0.9, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.9, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.9, + reportingTaxCalculated: 0.9, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.4, + taxableAmount: 40, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.4, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 40, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.4, + reportingTaxCalculated: 0.4, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "3", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "Shipping", + lineAmount: 77.51, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-23"), + tax: 7.36, + taxableAmount: 77.51, + taxCalculated: 7.36, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-23"), + taxIncluded: false, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 4.65, + taxableAmount: 77.51, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 4.65, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 77.51, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 4.65, + reportingTaxCalculated: 4.65, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.19, + taxableAmount: 77.51, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.19, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 77.51, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.19, + reportingTaxCalculated: 0.19, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 1.74, + taxableAmount: 77.51, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 1.74, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 77.51, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 1.74, + reportingTaxCalculated: 1.74, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.78, + taxableAmount: 77.51, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.78, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 77.51, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.78, + reportingTaxCalculated: 0.78, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + ], + summary: [ + { + country: "US", + region: "CA", + jurisType: JurisdictionType.State, + jurisCode: "06", + jurisName: "CALIFORNIA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Use", + taxSubType: "U", + taxName: "CA STATE TAX", + rateType: RateType.General, + taxable: 157.51, + rate: 0.06, + tax: 9.45, + taxCalculated: 9.45, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.County, + jurisCode: "037", + jurisName: "LOS ANGELES", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Use", + taxSubType: "U", + taxName: "CA COUNTY TAX", + rateType: RateType.General, + taxable: 157.51, + rate: 0.0025, + tax: 0.39, + taxCalculated: 0.39, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + taxAuthorityType: 45, + stateAssignedNo: "19", + taxType: "Use", + taxSubType: "U", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 157.51, + rate: 0.01, + tax: 1.58, + taxCalculated: 1.58, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + taxAuthorityType: 45, + stateAssignedNo: "594", + taxType: "Use", + taxSubType: "U", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 157.51, + rate: 0.0225, + tax: 3.54, + taxCalculated: 3.54, + nonTaxable: 0, + exemption: 0, + }, + ], +}; + +const taxIncludedNoShipping: Partial = { + totalAmount: 73.06, + totalExempt: 0, + totalDiscount: 0, + totalTax: 6.94, + totalTaxable: 73.06, + totalTaxCalculated: 6.94, lines: [ { id: 0, @@ -549,13 +1051,13 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { quantity: 2, ref1: "", ref2: "", - reportingDate: new Date(), + reportingDate: new Date("2023-05-23"), tax: 3.47, taxableAmount: 36.53, taxCalculated: 3.47, taxCode: "P0000000", taxCodeId: 8087, - taxDate: new Date(), + taxDate: new Date("2023-05-23"), taxIncluded: true, details: [ { @@ -712,18 +1214,611 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { exemptCertId: 0, exemptNo: "", isItemTaxable: true, + itemCode: "", + lineAmount: 36.53, + quantity: 2, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-23"), + tax: 3.47, + taxableAmount: 36.53, + taxCalculated: 3.47, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-23"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.19, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.19, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.19, + reportingTaxCalculated: 2.19, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.09, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.09, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.09, + reportingTaxCalculated: 0.09, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 0.82, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.82, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.82, + reportingTaxCalculated: 0.82, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.37, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.37, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.37, + reportingTaxCalculated: 0.37, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + ], + summary: [ + { + country: "US", + region: "CA", + jurisType: JurisdictionType.State, + jurisCode: "06", + jurisName: "CALIFORNIA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Use", + taxSubType: "U", + taxName: "CA STATE TAX", + rateType: RateType.General, + taxable: 73.06, + rate: 0.06, + tax: 4.38, + taxCalculated: 4.38, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.County, + jurisCode: "037", + jurisName: "LOS ANGELES", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Use", + taxSubType: "U", + taxName: "CA COUNTY TAX", + rateType: RateType.General, + taxable: 73.06, + rate: 0.0025, + tax: 0.18, + taxCalculated: 0.18, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + taxAuthorityType: 45, + stateAssignedNo: "19", + taxType: "Use", + taxSubType: "U", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 73.06, + rate: 0.01, + tax: 0.74, + taxCalculated: 0.74, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + taxAuthorityType: 45, + stateAssignedNo: "594", + taxType: "Use", + taxSubType: "U", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 73.06, + rate: 0.0225, + tax: 1.64, + taxCalculated: 1.64, + nonTaxable: 0, + exemption: 0, + }, + ], +}; + +const taxIncludedShipping: Partial = { + totalAmount: 143.84, + totalExempt: 0, + totalDiscount: 0, + totalTax: 13.67, + totalTaxable: 143.84, + totalTaxCalculated: 13.67, + lines: [ + { + id: 0, + transactionId: 0, + lineNumber: "1", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 36.53, + quantity: 2, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-23"), + tax: 3.47, + taxableAmount: 36.53, + taxCalculated: 3.47, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-23"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.19, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.19, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.19, + reportingTaxCalculated: 2.19, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.09, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.09, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.09, + reportingTaxCalculated: 0.09, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 0.82, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.82, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.82, + reportingTaxCalculated: 0.82, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.37, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.37, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.37, + reportingTaxCalculated: 0.37, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "2", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 36.53, + quantity: 2, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-23"), + tax: 3.47, + taxableAmount: 36.53, + taxCalculated: 3.47, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-23"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.19, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.19, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.19, + reportingTaxCalculated: 2.19, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.09, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.09, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.09, + reportingTaxCalculated: 0.09, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 0.82, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.82, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.82, + reportingTaxCalculated: 0.82, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.37, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.37, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.37, + reportingTaxCalculated: 0.37, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "3", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, itemCode: "Shipping", lineAmount: 70.78, quantity: 1, ref1: "", ref2: "", - reportingDate: new Date(), + reportingDate: new Date("2023-05-23"), tax: 6.73, taxableAmount: 70.78, taxCalculated: 6.73, taxCode: "P0000000", taxCodeId: 8087, - taxDate: new Date(), + taxDate: new Date("2023-05-23"), taxIncluded: true, details: [ { @@ -870,38 +1965,6 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { vatNumberTypeId: 0, }, ], - addresses: [ - { - id: 0, - transactionId: 0, - boundaryLevel: BoundaryLevel.Zip5, - line1: "123 Palm Grove Ln", - line2: "", - line3: "", - city: "LOS ANGELES", - region: "CA", - postalCode: "90002", - country: "US", - taxRegionId: 4017056, - latitude: "33.948712", - longitude: "-118.245951", - }, - { - id: 0, - transactionId: 0, - boundaryLevel: BoundaryLevel.Zip5, - line1: "8559 Lake Avenue", - line2: "", - line3: "", - city: "New York", - region: "NY", - postalCode: "10001", - country: "US", - taxRegionId: 2088629, - latitude: "40.748481", - longitude: "-73.993125", - }, - ], summary: [ { country: "US", @@ -915,10 +1978,10 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA STATE TAX", rateType: RateType.General, - taxable: 107.31, + taxable: 80, rate: 0.06, - tax: 6.44, - taxCalculated: 6.44, + tax: 4.8, + taxCalculated: 4.8, nonTaxable: 0, exemption: 0, }, @@ -934,10 +1997,10 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA COUNTY TAX", rateType: RateType.General, - taxable: 107.31, + taxable: 80, rate: 0.0025, - tax: 0.27, - taxCalculated: 0.27, + tax: 0.2, + taxCalculated: 0.2, nonTaxable: 0, exemption: 0, }, @@ -953,10 +2016,10 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA SPECIAL TAX", rateType: RateType.General, - taxable: 107.31, + taxable: 80, rate: 0.01, - tax: 1.08, - taxCalculated: 1.08, + tax: 0.8, + taxCalculated: 0.8, nonTaxable: 0, exemption: 0, }, @@ -972,52 +2035,23 @@ const TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK: TransactionModel = { taxSubType: "U", taxName: "CA SPECIAL TAX", rateType: RateType.General, - taxable: 107.31, + taxable: 80, rate: 0.0225, - tax: 2.41, - taxCalculated: 2.41, + tax: 1.8, + taxCalculated: 1.8, nonTaxable: 0, exemption: 0, }, ], }; -const NON_TAXABLE_TRANSACTION_MOCK: TransactionModel = { - id: 0, - code: "d431d046-f0b4-4eb3-a412-b2405323d148", - companyId: 7799640, - date: new Date(), - paymentDate: new Date(), - status: DocumentStatus.Temporary, - type: DocumentType.SalesOrder, - batchCode: "", - currencyCode: "USD", - exchangeRateCurrencyCode: "USD", - customerUsageType: "", - entityUseCode: "", - customerVendorCode: "VXNlcjoyMDg0NTEwNDEx", - customerCode: "VXNlcjoyMDg0NTEwNDEx", - exemptNo: "", - reconciled: false, - locationCode: "", - reportingLocationCode: "", - purchaseOrderNo: "", - referenceCode: "", - salespersonCode: "", - totalAmount: 97.51, - totalExempt: 97.51, +const nonTaxable: Partial = { + totalAmount: 80, + totalExempt: 0, totalDiscount: 0, totalTax: 0, totalTaxable: 0, totalTaxCalculated: 0, - adjustmentReason: AdjustmentReason.NotAdjusted, - locked: false, - version: 1, - exchangeRateEffectiveDate: new Date(), - exchangeRate: 1, - modifiedDate: new Date(), - modifiedUserId: 6479978, - taxDate: new Date(), lines: [ { id: 0, @@ -1152,38 +2186,6 @@ const NON_TAXABLE_TRANSACTION_MOCK: TransactionModel = { vatNumberTypeId: 0, }, ], - addresses: [ - { - id: 0, - transactionId: 0, - boundaryLevel: BoundaryLevel.Address, - line1: "38774 Apple Ct", - line2: "", - line3: "", - city: "OCEAN VIEW", - region: "DE", - postalCode: "19970", - country: "US", - taxRegionId: 4014271, - latitude: "38.58276", - longitude: "-75.074325", - }, - { - id: 0, - transactionId: 0, - boundaryLevel: BoundaryLevel.Zip5, - line1: "8559 Lake Avenue", - line2: "", - line3: "", - city: "New York", - region: "NY", - postalCode: "10001", - country: "US", - taxRegionId: 2088629, - latitude: "40.748481", - longitude: "-73.993125", - }, - ], summary: [ { country: "US", @@ -1201,18 +2203,18 @@ const NON_TAXABLE_TRANSACTION_MOCK: TransactionModel = { rate: 0, tax: 0, taxCalculated: 0, - nonTaxable: 97.51, + nonTaxable: 80, exemption: 0, }, ], }; -const TAXABLE_TAX_NOT_INCLUDED_TRANSACTION_MOCK: TransactionModel = { +const defaultTransaction: TransactionModel = { id: 0, - code: "393b71d9-a102-4726-bba4-061832f526a2", + code: "fd334912-db04-4479-8e8b-c274c68ff4b5", companyId: 7799660, - date: new Date(), - paymentDate: new Date(), + date: new Date(new Date("2023-05-23")), + paymentDate: new Date(new Date("2023-05-23")), status: DocumentStatus.Temporary, type: DocumentType.SalesOrder, batchCode: "", @@ -1229,358 +2231,16 @@ const TAXABLE_TAX_NOT_INCLUDED_TRANSACTION_MOCK: TransactionModel = { purchaseOrderNo: "", referenceCode: "", salespersonCode: "", - totalAmount: 117.51, - totalExempt: 0, - totalDiscount: 0, - totalTax: 11.16, - totalTaxable: 117.51, - totalTaxCalculated: 11.16, + adjustmentReason: AdjustmentReason.NotAdjusted, locked: false, version: 1, - exchangeRateEffectiveDate: new Date(), + exchangeRateEffectiveDate: new Date(new Date("2023-05-23")), exchangeRate: 1, - modifiedDate: new Date(), + modifiedDate: new Date(new Date("2023-05-23T09:49:36.29194Z")), modifiedUserId: 6479978, - taxDate: new Date(), - lines: [ - { - id: 0, - transactionId: 0, - lineNumber: "1", - customerUsageType: "", - entityUseCode: "", - discountAmount: 0, - exemptAmount: 0, - exemptCertId: 0, - exemptNo: "", - isItemTaxable: true, - itemCode: "", - lineAmount: 40, - quantity: 2, - ref1: "", - ref2: "", - reportingDate: new Date(), - tax: 3.8, - taxableAmount: 40, - taxCalculated: 3.8, - taxCode: "P0000000", - taxCodeId: 8087, - taxDate: new Date(), - taxIncluded: false, - details: [ - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "06", - jurisName: "CALIFORNIA", - stateAssignedNo: "", - jurisType: JurisTypeId.STJ, - jurisdictionType: JurisdictionType.State, - nonTaxableAmount: 0, - rate: 0.06, - tax: 2.4, - taxableAmount: 40, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA STATE TAX", - taxAuthorityTypeId: 45, - taxCalculated: 2.4, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 40, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 2.4, - reportingTaxCalculated: 2.4, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "037", - jurisName: "LOS ANGELES", - stateAssignedNo: "", - jurisType: JurisTypeId.CTY, - jurisdictionType: JurisdictionType.County, - nonTaxableAmount: 0, - rate: 0.0025, - tax: 0.1, - taxableAmount: 40, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA COUNTY TAX", - taxAuthorityTypeId: 45, - taxCalculated: 0.1, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 40, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 0.1, - reportingTaxCalculated: 0.1, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "EMAR0", - jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", - stateAssignedNo: "594", - jurisType: JurisTypeId.STJ, - jurisdictionType: JurisdictionType.Special, - nonTaxableAmount: 0, - rate: 0.0225, - tax: 0.9, - taxableAmount: 40, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA SPECIAL TAX", - taxAuthorityTypeId: 45, - taxCalculated: 0.9, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 40, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 0.9, - reportingTaxCalculated: 0.9, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "EMTC0", - jurisName: "LOS ANGELES CO LOCAL TAX SL", - stateAssignedNo: "19", - jurisType: JurisTypeId.STJ, - jurisdictionType: JurisdictionType.Special, - nonTaxableAmount: 0, - rate: 0.01, - tax: 0.4, - taxableAmount: 40, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA SPECIAL TAX", - taxAuthorityTypeId: 45, - taxCalculated: 0.4, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 40, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 0.4, - reportingTaxCalculated: 0.4, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - ], - nonPassthroughDetails: [], - hsCode: "", - costInsuranceFreight: 0, - vatCode: "", - vatNumberTypeId: 0, - }, - { - id: 0, - transactionId: 0, - lineNumber: "2", - customerUsageType: "", - entityUseCode: "", - discountAmount: 0, - exemptAmount: 0, - exemptCertId: 0, - exemptNo: "", - isItemTaxable: true, - itemCode: "Shipping", - lineAmount: 77.51, - quantity: 1, - ref1: "", - ref2: "", - reportingDate: new Date(), - tax: 7.36, - taxableAmount: 77.51, - taxCalculated: 7.36, - taxCode: "P0000000", - taxCodeId: 8087, - taxDate: new Date(), - taxIncluded: false, - details: [ - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "06", - jurisName: "CALIFORNIA", - stateAssignedNo: "", - jurisType: JurisTypeId.STA, - jurisdictionType: JurisdictionType.State, - nonTaxableAmount: 0, - rate: 0.06, - tax: 4.65, - taxableAmount: 77.51, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA STATE TAX", - taxAuthorityTypeId: 45, - taxCalculated: 4.65, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 77.51, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 4.65, - reportingTaxCalculated: 4.65, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "037", - jurisName: "LOS ANGELES", - stateAssignedNo: "", - jurisType: JurisTypeId.CTY, - jurisdictionType: JurisdictionType.County, - nonTaxableAmount: 0, - rate: 0.0025, - tax: 0.19, - taxableAmount: 77.51, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA COUNTY TAX", - taxAuthorityTypeId: 45, - taxCalculated: 0.19, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 77.51, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 0.19, - reportingTaxCalculated: 0.19, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "EMAR0", - jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", - stateAssignedNo: "594", - jurisType: JurisTypeId.STJ, - jurisdictionType: JurisdictionType.Special, - nonTaxableAmount: 0, - rate: 0.0225, - tax: 1.74, - taxableAmount: 77.51, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA SPECIAL TAX", - taxAuthorityTypeId: 45, - taxCalculated: 1.74, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 77.51, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 1.74, - reportingTaxCalculated: 1.74, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - { - id: 0, - transactionLineId: 0, - transactionId: 0, - country: "US", - region: "CA", - exemptAmount: 0, - jurisCode: "EMTC0", - jurisName: "LOS ANGELES CO LOCAL TAX SL", - stateAssignedNo: "19", - jurisType: JurisTypeId.STJ, - jurisdictionType: JurisdictionType.Special, - nonTaxableAmount: 0, - rate: 0.01, - tax: 0.78, - taxableAmount: 77.51, - taxType: "Use", - taxSubTypeId: "U", - taxName: "CA SPECIAL TAX", - taxAuthorityTypeId: 45, - taxCalculated: 0.78, - rateType: RateType.General, - rateTypeCode: "G", - unitOfBasis: "PerCurrencyUnit", - isNonPassThru: false, - isFee: false, - reportingTaxableUnits: 77.51, - reportingNonTaxableUnits: 0, - reportingExemptUnits: 0, - reportingTax: 0.78, - reportingTaxCalculated: 0.78, - liabilityType: LiabilityType.Seller, - chargedTo: ChargedTo.Buyer, - }, - ], - nonPassthroughDetails: [], - hsCode: "", - costInsuranceFreight: 0, - vatCode: "", - vatNumberTypeId: 0, - }, - ], + taxDate: new Date(new Date("2023-05-23")), + lines: [], addresses: [ { id: 0, @@ -1613,191 +2273,29 @@ const TAXABLE_TAX_NOT_INCLUDED_TRANSACTION_MOCK: TransactionModel = { longitude: "-73.993125", }, ], - summary: [ - { - country: "US", - region: "CA", - jurisType: JurisdictionType.State, - jurisCode: "06", - jurisName: "CALIFORNIA", - taxAuthorityType: 45, - stateAssignedNo: "", - taxType: "Use", - taxSubType: "U", - taxName: "CA STATE TAX", - rateType: RateType.General, - taxable: 117.51, - rate: 0.06, - tax: 7.05, - taxCalculated: 7.05, - nonTaxable: 0, - exemption: 0, - }, - { - country: "US", - region: "CA", - jurisType: JurisdictionType.County, - jurisCode: "037", - jurisName: "LOS ANGELES", - taxAuthorityType: 45, - stateAssignedNo: "", - taxType: "Use", - taxSubType: "U", - taxName: "CA COUNTY TAX", - rateType: RateType.General, - taxable: 117.51, - rate: 0.0025, - tax: 0.29, - taxCalculated: 0.29, - nonTaxable: 0, - exemption: 0, - }, - { - country: "US", - region: "CA", - jurisType: JurisdictionType.Special, - jurisCode: "EMTC0", - jurisName: "LOS ANGELES CO LOCAL TAX SL", - taxAuthorityType: 45, - stateAssignedNo: "19", - taxType: "Use", - taxSubType: "U", - taxName: "CA SPECIAL TAX", - rateType: RateType.General, - taxable: 117.51, - rate: 0.01, - tax: 1.18, - taxCalculated: 1.18, - nonTaxable: 0, - exemption: 0, - }, - { - country: "US", - region: "CA", - jurisType: JurisdictionType.Special, - jurisCode: "EMAR0", - jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", - taxAuthorityType: 45, - stateAssignedNo: "594", - taxType: "Use", - taxSubType: "U", - taxName: "CA SPECIAL TAX", - rateType: RateType.General, - taxable: 117.51, - rate: 0.0225, - tax: 2.64, - taxCalculated: 2.64, - nonTaxable: 0, - exemption: 0, - }, - ], + summary: [], }; -const MOCKED_CALCULATE_TAXES_ARGS: AvataxCalculateTaxesMapPayloadArgs = { - taxBase: { - pricesEnteredWithTax: false, - currency: "PLN", - channel: { - slug: "channel-pln", - }, - sourceObject: { - __typename: "Order", - user: { - id: "VXNlcjo5ZjY3ZjY0Zi1iZjY5LTQ5ZjYtYjQ4Zi1iZjY3ZjY0ZjY0ZjY=", - }, - }, - discounts: [], - address: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - country: { - code: "US", - }, - countryArea: "CA", - postalCode: "90002", - }, - shippingPrice: { - amount: 48.33, - }, - lines: [ - { - quantity: 3, - unitPrice: { - amount: 84, - }, - totalPrice: { - amount: 252, - }, - sourceLine: { - __typename: "OrderLine", - id: "T3JkZXJMaW5lOmY1NGQ1MWY2LTc1OTctNGY2OC1hNDk0LTFjYjZlYjRmOTlhMQ==", - variant: { - id: "UHJvZHVjdFZhcmlhbnQ6MzQ2", - product: { - metafield: null, - productType: { - metafield: null, - }, - }, - }, - }, - }, - { - quantity: 1, - unitPrice: { - amount: 5.99, - }, - totalPrice: { - amount: 5.99, - }, - sourceLine: { - __typename: "OrderLine", - id: "T3JkZXJMaW5lOjU1NTFjNTFjLTM5MWQtNGI0Ny04MGU0LWVjY2Q5ZjU4MjQyNQ==", - variant: { - id: "UHJvZHVjdFZhcmlhbnQ6Mzg1", - product: { - metafield: null, - productType: { - metafield: null, - }, - }, - }, - }, - }, - ], - }, - channel: { - providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081", - enabled: false, - address: { - country: "US", - zip: "92093", - state: "17-05-2023", - city: "La Jolla", - street: "9500 Gilman Drive", - }, - }, - config: { - companyCode: "DEFAULT", - isAutocommit: false, - isSandbox: true, - name: "Avatax-1", - password: "password", - username: "username", - shippingTaxCode: "FR000000", - }, +const mockTransactionVariants = { + taxExcludedNoShipping, + taxExcludedShipping, + taxIncludedNoShipping, + taxIncludedShipping, + nonTaxable, }; -export const transactionModelMocks = { - taxable: { - taxIncluded: TAXABLE_TAX_INCLUDED_TRANSACTION_MOCK, - taxNotIncluded: TAXABLE_TAX_NOT_INCLUDED_TRANSACTION_MOCK, - }, - nonTaxable: NON_TAXABLE_TRANSACTION_MOCK, - noShippingLine: NO_SHIPPING_TRANSACTION_MOCK, -}; +type MockTransactionVariant = keyof typeof mockTransactionVariants; -export const mapPayloadArgsMocks = { - default: MOCKED_CALCULATE_TAXES_ARGS, +const createMockTransaction = ( + variant: MockTransactionVariant, + overrides?: Partial +): TransactionModel => ({ + ...defaultTransaction, + ...mockTransactionVariants[variant], + ...overrides, +}); + +// todo: convert to a mock-generator +export const avataxMockTransactionFactory = { + createMockTransaction, }; diff --git a/apps/taxes/src/modules/avatax/avatax-webhook.service.ts b/apps/taxes/src/modules/avatax/avatax-webhook.service.ts index 2e7a17b..b66de68 100644 --- a/apps/taxes/src/modules/avatax/avatax-webhook.service.ts +++ b/apps/taxes/src/modules/avatax/avatax-webhook.service.ts @@ -3,14 +3,14 @@ import { OrderFulfilledSubscriptionFragment, TaxBaseFragment, } from "../../../generated/graphql"; -import { createLogger, Logger } from "../../lib/logger"; +import { Logger, createLogger } from "../../lib/logger"; import { ChannelConfig } from "../channels-configuration/channels-config"; import { ProviderWebhookService } from "../taxes/tax-provider-webhook"; -import { avataxCalculateTaxesMaps } from "./maps/avatax-calculate-taxes-map"; import { AvataxClient } from "./avatax-client"; import { AvataxConfig, defaultAvataxConfig } from "./avatax-config"; -import { avataxOrderCreatedMaps } from "./maps/avatax-order-created-map"; -import { avataxOrderFulfilledMaps } from "./maps/avatax-order-fulfilled-map"; +import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter"; +import { AvataxOrderCreatedAdapter } from "./order-created/avatax-order-created-adapter"; +import { AvataxOrderFulfilledAdapter } from "./order-fulfilled/avatax-order-fulfilled-adapter"; export class AvataxWebhookService implements ProviderWebhookService { config = defaultAvataxConfig; @@ -29,38 +29,38 @@ export class AvataxWebhookService implements ProviderWebhookService { this.client = avataxClient; } - async calculateTaxes(payload: TaxBaseFragment, channel: ChannelConfig) { - this.logger.debug({ payload, channel }, "calculateTaxes called with:"); - const args = avataxCalculateTaxesMaps.mapPayload({ - taxBase: payload, - channel, - config: this.config, - }); - const result = await this.client.createTransaction(args); + async calculateTaxes(taxBase: TaxBaseFragment, channelConfig: ChannelConfig) { + this.logger.debug({ taxBase, channelConfig }, "calculateTaxes called with:"); + const adapter = new AvataxCalculateTaxesAdapter(this.config); - this.logger.debug({ result }, "calculateTaxes response"); - return avataxCalculateTaxesMaps.mapResponse(result); + const response = await adapter.send({ channelConfig, taxBase }); + + this.logger.debug({ response }, "calculateTaxes response:"); + + return response; } - async createOrder(order: OrderCreatedSubscriptionFragment, channel: ChannelConfig) { - this.logger.debug({ order, channel }, "createOrder called with:"); - const model = avataxOrderCreatedMaps.mapPayload({ order, channel, config: this.config }); + async createOrder(order: OrderCreatedSubscriptionFragment, channelConfig: ChannelConfig) { + this.logger.debug({ order, channelConfig }, "createOrder called with:"); - this.logger.debug({ model }, "will call createTransaction with"); - const result = await this.client.createTransaction(model); + const adapter = new AvataxOrderCreatedAdapter(this.config); - this.logger.debug({ result }, "createOrder response"); - return avataxOrderCreatedMaps.mapResponse(result); + const response = await adapter.send({ channelConfig, order }); + + this.logger.debug({ response }, "createOrder response:"); + + return response; } - async fulfillOrder(order: OrderFulfilledSubscriptionFragment, channel: ChannelConfig) { - this.logger.debug({ order, channel }, "fulfillOrder called with:"); - const args = avataxOrderFulfilledMaps.mapPayload({ order, config: this.config }); + async fulfillOrder(order: OrderFulfilledSubscriptionFragment, channelConfig: ChannelConfig) { + this.logger.debug({ order, channelConfig }, "fulfillOrder called with:"); - this.logger.debug({ args }, "will call commitTransaction with"); - const result = await this.client.commitTransaction(args); + const adapter = new AvataxOrderFulfilledAdapter(this.config); - this.logger.debug({ result }, "fulfillOrder response"); - return { ok: true }; + const response = await adapter.send({ order }); + + this.logger.debug({ response }, "fulfillOrder response:"); + + return response; } } diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts new file mode 100644 index 0000000..32620de --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts @@ -0,0 +1,45 @@ +import { TaxBaseFragment } from "../../../../generated/graphql"; +import { Logger, createLogger } from "../../../lib/logger"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook"; +import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; +import { AvataxClient, CreateTransactionArgs } from "../avatax-client"; +import { AvataxConfig } from "../avatax-config"; +import { AvataxCalculateTaxesPayloadTransformer } from "./avatax-calculate-taxes-payload-transformer"; +import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxes-response-transformer"; + +export const SHIPPING_ITEM_CODE = "Shipping"; + +export type Payload = { + taxBase: TaxBaseFragment; + channelConfig: ChannelConfig; + config: AvataxConfig; +}; + +export type Target = CreateTransactionArgs; +export type Response = CalculateTaxesResponse; + +export class AvataxCalculateTaxesAdapter implements WebhookAdapter { + private logger: Logger; + constructor(private readonly config: AvataxConfig) { + this.logger = createLogger({ service: "AvataxCalculateTaxesAdapter" }); + } + + async send(payload: Pick): Promise { + this.logger.debug({ payload }, "send called with:"); + const payloadTransformer = new AvataxCalculateTaxesPayloadTransformer(); + const target = payloadTransformer.transform({ ...payload, config: this.config }); + + const client = new AvataxClient(this.config); + const response = await client.createTransaction(target); + + this.logger.debug({ response }, "Avatax createTransaction response:"); + + const responseTransformer = new AvataxCalculateTaxesResponseTransformer(); + const transformedResponse = responseTransformer.transform(response); + + this.logger.debug({ transformedResponse }, "Transformed Avatax createTransaction response to:"); + + return transformedResponse; + } +} diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts new file mode 100644 index 0000000..acc8f4b --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts @@ -0,0 +1,992 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { TaxBaseFragment } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { DocumentStatus } from "avatax/lib/enums/DocumentStatus"; +import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { AdjustmentReason } from "avatax/lib/enums/AdjustmentReason"; +import { JurisTypeId } from "avatax/lib/enums/JurisTypeId"; +import { LiabilityType } from "avatax/lib/enums/LiabilityType"; +import { RateType } from "avatax/lib/enums/RateType"; +import { ChargedTo } from "avatax/lib/enums/ChargedTo"; +import { JurisdictionType } from "avatax/lib/enums/JurisdictionType"; +import { BoundaryLevel } from "avatax/lib/enums/BoundaryLevel"; +import { AvataxConfig } from "../avatax-config"; + +type TaxBase = TaxBaseFragment; + +const defaultTaxBase: TaxBase = { + pricesEnteredWithTax: true, + currency: "USD", + channel: { + slug: "default-channel", + }, + discounts: [], + address: { + streetAddress1: "600 Montgomery St", + streetAddress2: "", + city: "SAN FRANCISCO", + countryArea: "CA", + postalCode: "94111", + country: { + code: "US", + }, + }, + shippingPrice: { + amount: 48.33, + }, + lines: [ + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjNmMjYwZmMyLTZjN2UtNGM5Ni1iYTMwLTEyMjAyODMzOTUyZA==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQ5", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 3, + unitPrice: { + amount: 20, + }, + totalPrice: { + amount: 60, + }, + }, + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjNlNGZjODdkLTIyMmEtNDZiYi1iYzIzLWJiYWVkODVlOTQ4Mg==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzUw", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 1, + unitPrice: { + amount: 20, + }, + totalPrice: { + amount: 20, + }, + }, + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOmM2NTBhMzVkLWQ1YjQtNGRhNy1hMjNjLWEzODU4ZDE1MzI2Mw==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQw", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 2, + unitPrice: { + amount: 50, + }, + totalPrice: { + amount: 100, + }, + }, + ], + sourceObject: { + user: { + id: "VXNlcjoyMDg0NTEwNDEw", + }, + }, +}; + +const defaultChannelConfig: ChannelConfig = { + providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081", + enabled: false, + address: { + country: "US", + zip: "92093", + state: "CA", + city: "La Jolla", + street: "9500 Gilman Drive", + }, +}; + +const defaultTransactionModel: TransactionModel = { + id: 0, + code: "aec372bb-f3b3-40fb-9d84-2b46cd67e516", + companyId: 7799660, + date: new Date("2023-05-25"), + paymentDate: new Date("2023-05-25"), + status: DocumentStatus.Temporary, + type: DocumentType.SalesOrder, + batchCode: "", + currencyCode: "USD", + exchangeRateCurrencyCode: "USD", + customerUsageType: "", + entityUseCode: "", + customerVendorCode: "VXNlcjoyMDg0NTEwNDEw", + customerCode: "VXNlcjoyMDg0NTEwNDEw", + exemptNo: "", + reconciled: false, + locationCode: "", + reportingLocationCode: "", + purchaseOrderNo: "", + referenceCode: "", + salespersonCode: "", + totalAmount: 137.34, + totalExempt: 0, + totalDiscount: 0, + totalTax: 11.83, + totalTaxable: 137.34, + totalTaxCalculated: 11.83, + adjustmentReason: AdjustmentReason.NotAdjusted, + locked: false, + version: 1, + exchangeRateEffectiveDate: new Date("2023-05-25"), + exchangeRate: 1, + modifiedDate: new Date("2023-05-25T10:23:15.317354Z"), + modifiedUserId: 6479978, + taxDate: new Date("2023-05-25"), + lines: [ + { + id: 0, + transactionId: 0, + lineNumber: "1", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 18.42, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-25"), + tax: 1.58, + taxableAmount: 18.42, + taxCalculated: 1.58, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-25"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 1.1, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 1.1, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 1.1, + reportingTaxCalculated: 1.1, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "085", + jurisName: "SANTA CLARA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.05, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.05, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.05, + reportingTaxCalculated: 0.05, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + stateAssignedNo: "940", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01375, + tax: 0.25, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.25, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.25, + reportingTaxCalculated: 0.25, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + stateAssignedNo: "43", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.18, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.18, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.18, + reportingTaxCalculated: 0.18, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "2", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 18.42, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-25"), + tax: 1.58, + taxableAmount: 18.42, + taxCalculated: 1.58, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-25"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 1.1, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 1.1, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 1.1, + reportingTaxCalculated: 1.1, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "085", + jurisName: "SANTA CLARA", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.05, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.05, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.05, + reportingTaxCalculated: 0.05, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + stateAssignedNo: "940", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01375, + tax: 0.25, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.25, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.25, + reportingTaxCalculated: 0.25, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + stateAssignedNo: "43", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.18, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.18, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.18, + reportingTaxCalculated: 0.18, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "3", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 46.03, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-25"), + tax: 3.97, + taxableAmount: 46.03, + taxCalculated: 3.97, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-25"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.76, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.76, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.76, + reportingTaxCalculated: 2.76, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "085", + jurisName: "SANTA CLARA", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.12, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.12, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.12, + reportingTaxCalculated: 0.12, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + stateAssignedNo: "940", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01375, + tax: 0.63, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.63, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.63, + reportingTaxCalculated: 0.63, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + stateAssignedNo: "43", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.46, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.46, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.46, + reportingTaxCalculated: 0.46, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "4", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "Shipping", + lineAmount: 54.47, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: new Date("2023-05-25"), + tax: 4.7, + taxableAmount: 54.47, + taxCalculated: 4.7, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date("2023-05-25"), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 3.27, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 3.27, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 3.27, + reportingTaxCalculated: 3.27, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "085", + jurisName: "SANTA CLARA", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.14, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.14, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.14, + reportingTaxCalculated: 0.14, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + stateAssignedNo: "940", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01375, + tax: 0.75, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.75, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.75, + reportingTaxCalculated: 0.75, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + stateAssignedNo: "43", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.54, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.54, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.54, + reportingTaxCalculated: 0.54, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + ], + addresses: [ + { + id: 0, + transactionId: 0, + boundaryLevel: BoundaryLevel.Address, + line1: "600 Montgomery St", + line2: "", + line3: "", + city: "SAN FRANCISCO", + region: "CA", + postalCode: "94111", + country: "US", + taxRegionId: 4024330, + latitude: "37.795255", + longitude: "-122.40313", + }, + { + id: 0, + transactionId: 0, + boundaryLevel: BoundaryLevel.Address, + line1: "33 N. First Street", + line2: "", + line3: "", + city: "Campbell", + region: "CA", + postalCode: "95008", + country: "US", + taxRegionId: 2128577, + latitude: "37.287589", + longitude: "-121.944955", + }, + ], + summary: [ + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "06", + jurisName: "CALIFORNIA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Sales", + taxSubType: "S", + taxName: "CA STATE TAX", + rateType: RateType.General, + taxable: 137.34, + rate: 0.06, + tax: 8.23, + taxCalculated: 8.23, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.County, + jurisCode: "085", + jurisName: "SANTA CLARA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Sales", + taxSubType: "S", + taxName: "CA COUNTY TAX", + rateType: RateType.General, + taxable: 137.34, + rate: 0.0025, + tax: 0.36, + taxCalculated: 0.36, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.County, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + taxAuthorityType: 45, + stateAssignedNo: "940", + taxType: "Sales", + taxSubType: "S", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 137.34, + rate: 0.01375, + tax: 1.88, + taxCalculated: 1.88, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + taxAuthorityType: 45, + stateAssignedNo: "43", + taxType: "Sales", + taxSubType: "S", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 137.34, + rate: 0.01, + tax: 1.36, + taxCalculated: 1.36, + nonTaxable: 0, + exemption: 0, + }, + ], +}; + +const defaultAvataxConfig: AvataxConfig = { + companyCode: "DEFAULT", + isAutocommit: false, + isSandbox: true, + name: "Avatax-1", + password: "password", + username: "username", + shippingTaxCode: "FR000000", +}; + +const testingScenariosMap = { + default: { + taxBase: defaultTaxBase, + channelConfig: defaultChannelConfig, + avataxConfig: defaultAvataxConfig, + response: defaultTransactionModel, + }, +}; + +type TestingScenario = keyof typeof testingScenariosMap; + +export class AvataxCalculateTaxesMockGenerator { + constructor(private scenario: TestingScenario = "default") {} + generateTaxBase = (overrides: Partial = {}): TaxBase => + structuredClone({ + ...testingScenariosMap[this.scenario].taxBase, + ...overrides, + }); + + generateChannelConfig = (overrides: Partial = {}): ChannelConfig => + structuredClone({ + ...testingScenariosMap[this.scenario].channelConfig, + ...overrides, + }); + + generateAvataxConfig = (overrides: Partial = {}): AvataxConfig => + structuredClone({ + ...testingScenariosMap[this.scenario].avataxConfig, + ...overrides, + }); + + generateResponse = (overrides: Partial = {}): TransactionModel => + structuredClone({ + ...testingScenariosMap[this.scenario].response, + ...overrides, + }); +} diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts new file mode 100644 index 0000000..e75741f --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; +import { AvataxCalculateTaxesMockGenerator } from "./avatax-calculate-taxes-mock-generator"; +import { + AvataxCalculateTaxesPayloadTransformer, + mapPayloadLines, +} from "./avatax-calculate-taxes-payload-transformer"; + +describe("AvataxCalculateTaxesPayloadTransformer", () => { + it("when discounts, calculates the sum of discounts", () => { + const mockGenerator = new AvataxCalculateTaxesMockGenerator(); + const avataxConfigMock = mockGenerator.generateAvataxConfig(); + const taxBaseMock = mockGenerator.generateTaxBase({ discounts: [{ amount: { amount: 10 } }] }); + + const payload = new AvataxCalculateTaxesPayloadTransformer().transform({ + taxBase: taxBaseMock, + channelConfig: mockGenerator.generateChannelConfig(), + config: avataxConfigMock, + }); + + expect(payload.model.discount).toEqual(10); + }); + it("when no discounts, the sum of discount is 0", () => { + const mockGenerator = new AvataxCalculateTaxesMockGenerator(); + const avataxConfigMock = mockGenerator.generateAvataxConfig(); + const taxBaseMock = mockGenerator.generateTaxBase(); + + const payload = new AvataxCalculateTaxesPayloadTransformer().transform({ + taxBase: taxBaseMock, + channelConfig: mockGenerator.generateChannelConfig(), + config: avataxConfigMock, + }); + + expect(payload.model.discount).toEqual(0); + }); +}); + +describe("mapPayloadLines", () => { + it("map lines and adds shipping as line", () => { + const mockGenerator = new AvataxCalculateTaxesMockGenerator(); + const avataxConfigMock = mockGenerator.generateAvataxConfig(); + const taxBaseMock = mockGenerator.generateTaxBase(); + const lines = mapPayloadLines(taxBaseMock, avataxConfigMock); + + expect(lines).toEqual([ + { + amount: 60, + quantity: 3, + taxCode: "", + taxIncluded: true, + discounted: false, + }, + { + amount: 20, + quantity: 1, + taxCode: "", + taxIncluded: true, + discounted: false, + }, + { + amount: 100, + quantity: 2, + taxCode: "", + taxIncluded: true, + discounted: false, + }, + { + amount: 48.33, + itemCode: "Shipping", + quantity: 1, + taxCode: "FR000000", + taxIncluded: true, + discounted: false, + }, + ]); + }); + it("when no shipping in tax base, does not add shipping as line", () => { + const mockGenerator = new AvataxCalculateTaxesMockGenerator(); + const avataxConfigMock = mockGenerator.generateAvataxConfig(); + const taxBaseMock = mockGenerator.generateTaxBase({ shippingPrice: { amount: 0 } }); + + const lines = mapPayloadLines(taxBaseMock, avataxConfigMock); + + expect(lines).toEqual([ + { + amount: 60, + quantity: 3, + taxCode: "", + taxIncluded: true, + discounted: false, + }, + { + amount: 20, + quantity: 1, + taxCode: "", + taxIncluded: true, + discounted: false, + }, + { + amount: 100, + quantity: 2, + taxCode: "", + taxIncluded: true, + discounted: false, + }, + ]); + }); + it("when discounts, sets discounted to true", () => { + const mockGenerator = new AvataxCalculateTaxesMockGenerator(); + const avataxConfigMock = mockGenerator.generateAvataxConfig(); + const taxBaseMock = mockGenerator.generateTaxBase({ discounts: [{ amount: { amount: 10 } }] }); + + const lines = mapPayloadLines(taxBaseMock, avataxConfigMock); + + expect(lines).toEqual([ + { + amount: 60, + quantity: 3, + taxCode: "", + taxIncluded: true, + discounted: true, + }, + { + amount: 20, + quantity: 1, + taxCode: "", + taxIncluded: true, + discounted: true, + }, + { + amount: 100, + quantity: 2, + taxCode: "", + taxIncluded: true, + discounted: true, + }, + { + amount: 48.33, + discounted: true, + itemCode: "Shipping", + quantity: 1, + taxCode: "FR000000", + taxIncluded: true, + }, + ]); + }); +}); diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts new file mode 100644 index 0000000..d6b2018 --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts @@ -0,0 +1,61 @@ +import { LineItemModel } from "avatax/lib/models/LineItemModel"; +import { TaxBaseFragment } from "../../../../generated/graphql"; +import { AvataxConfig } from "../avatax-config"; +import { avataxAddressFactory } from "../address-factory"; +import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { SHIPPING_ITEM_CODE, Payload, Target } from "./avatax-calculate-taxes-adapter"; +import { discountUtils } from "../../taxes/discount-utils"; + +export function mapPayloadLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] { + const isDiscounted = taxBase.discounts.length > 0; + const productLines: LineItemModel[] = taxBase.lines.map((line) => ({ + amount: line.totalPrice.amount, + taxIncluded: taxBase.pricesEnteredWithTax, + // todo: get from tax code matcher + taxCode: "", + quantity: line.quantity, + discounted: isDiscounted, + })); + + if (taxBase.shippingPrice.amount !== 0) { + // * In Avatax, shipping is a regular line + const shippingLine: LineItemModel = { + amount: taxBase.shippingPrice.amount, + itemCode: SHIPPING_ITEM_CODE, + taxCode: config.shippingTaxCode, + quantity: 1, + taxIncluded: taxBase.pricesEnteredWithTax, + discounted: isDiscounted, + }; + + return [...productLines, shippingLine]; + } + + return productLines; +} + +export class AvataxCalculateTaxesPayloadTransformer { + transform(props: Payload): Target { + const { taxBase, channelConfig, config } = props; + + return { + model: { + type: DocumentType.SalesOrder, + customerCode: taxBase.sourceObject.user?.id ?? "", + companyCode: config.companyCode, + // * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit + commit: config.isAutocommit, + addresses: { + shipFrom: avataxAddressFactory.fromChannelAddress(channelConfig.address), + shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!), + }, + currencyCode: taxBase.currency, + lines: mapPayloadLines(taxBase, config), + date: new Date(), + discount: discountUtils.sumDiscounts( + taxBase.discounts.map((discount) => discount.amount.amount) + ), + }, + }; + } +} diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.test.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.test.ts new file mode 100644 index 0000000..2db487d --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { avataxMockFactory } from "../avatax-mock-factory"; +import { AvataxCalculateTaxesResponseLinesTransformer } from "./avatax-calculate-taxes-response-lines-transformer"; + +const transformer = new AvataxCalculateTaxesResponseLinesTransformer(); + +const NON_TAXABLE_TRANSACTION_MOCK = avataxMockFactory.createMockTransaction("nonTaxable"); +const TAX_INCLUDED_TRANSACTION_MOCK = + avataxMockFactory.createMockTransaction("taxIncludedShipping"); +const TAX_EXCLUDED_TRANSACTION_MOCK = + avataxMockFactory.createMockTransaction("taxExcludedShipping"); + +describe("AvataxCalculateTaxesResponseLinesTransformer", () => { + it("when product lines are not taxable, returns line amount", () => { + const nonTaxableProductLines = transformer.transform(NON_TAXABLE_TRANSACTION_MOCK); + + expect(nonTaxableProductLines).toEqual([ + { + total_gross_amount: 20, + total_net_amount: 20, + tax_rate: 0, + }, + ]); + }); + + it("when product lines are taxable and tax is included, returns calculated gross & net amounts", () => { + const taxableProductLines = transformer.transform(TAX_INCLUDED_TRANSACTION_MOCK); + + expect(taxableProductLines).toEqual([ + { + total_gross_amount: 40, + total_net_amount: 36.53, + tax_rate: 0, + }, + { + total_gross_amount: 40, + total_net_amount: 36.53, + tax_rate: 0, + }, + ]); + }); + + it("when product lines are taxable and tax is not included, returns calculated gross & net amounts", () => { + const taxableProductLines = transformer.transform(TAX_EXCLUDED_TRANSACTION_MOCK); + + expect(taxableProductLines).toEqual([ + { + total_gross_amount: 43.8, + total_net_amount: 40, + tax_rate: 0, + }, + { + total_gross_amount: 43.8, + total_net_amount: 40, + tax_rate: 0, + }, + ]); + }); +}); diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts new file mode 100644 index 0000000..70c2d16 --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts @@ -0,0 +1,49 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { numbers } from "../../taxes/numbers"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; +import { Response, SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter"; + +export class AvataxCalculateTaxesResponseLinesTransformer { + transform(transaction: TransactionModel): Response["lines"] { + const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE); + + return ( + productLines?.map((line) => { + if (!line.isItemTaxable) { + return { + total_gross_amount: taxProviderUtils.resolveOptionalOrThrow( + line.lineAmount, + new Error("line.lineAmount is undefined") + ), + total_net_amount: taxProviderUtils.resolveOptionalOrThrow( + line.lineAmount, + new Error("line.lineAmount is undefined") + ), + tax_rate: 0, + }; + } + + const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow( + line.taxCalculated, + new Error("line.taxCalculated is undefined") + ); + const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow( + line.taxableAmount, + new Error("line.taxableAmount is undefined") + ); + const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals( + lineTotalNetAmount + lineTaxCalculated + ); + + return { + total_gross_amount: lineTotalGrossAmount, + total_net_amount: lineTotalNetAmount, + /* + * avatax doesnt return combined tax rate + * // todo: calculate percentage tax rate + */ tax_rate: 0, + }; + }) ?? [] + ); + } +} diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-shipping-transformer.test.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-shipping-transformer.test.ts new file mode 100644 index 0000000..cd975fe --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-shipping-transformer.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { avataxMockFactory } from "../avatax-mock-factory"; +import { AvataxCalculateTaxesResponseShippingTransformer } from "./avatax-calculate-taxes-response-shipping-transformer"; + +const transformer = new AvataxCalculateTaxesResponseShippingTransformer(); + +const TAX_EXCLUDED_NO_SHIPPING_TRANSACTION_MOCK = + avataxMockFactory.createMockTransaction("taxExcludedNoShipping"); +const NON_TAXABLE_TRANSACTION_MOCK = avataxMockFactory.createMockTransaction("nonTaxable"); +const TAX_INCLUDED_SHIPPING_TRANSACTION_MOCK = + avataxMockFactory.createMockTransaction("taxIncludedShipping"); +const TAX_EXCLUDED_SHIPPING_TRANSACTION_MOCK = + avataxMockFactory.createMockTransaction("taxExcludedShipping"); + +describe("AvataxCalculateTaxesResponseShippingTransformer", () => { + it("when shipping line is not present, returns 0s", () => { + const shippingLine = transformer.transform(TAX_EXCLUDED_NO_SHIPPING_TRANSACTION_MOCK); + + expect(shippingLine).toEqual({ + shipping_price_gross_amount: 0, + shipping_price_net_amount: 0, + shipping_tax_rate: 0, + }); + }); + it("when shipping line is not taxable, returns line amount", () => { + const nonTaxableShippingLine = transformer.transform(NON_TAXABLE_TRANSACTION_MOCK); + + expect(nonTaxableShippingLine).toEqual({ + shipping_price_gross_amount: 77.51, + shipping_price_net_amount: 77.51, + shipping_tax_rate: 0, + }); + }); + + it("when shipping line is taxable and tax is included, returns calculated gross & net amounts", () => { + const taxableShippingLine = transformer.transform(TAX_INCLUDED_SHIPPING_TRANSACTION_MOCK); + + expect(taxableShippingLine).toEqual({ + shipping_price_gross_amount: 77.51, + shipping_price_net_amount: 70.78, + shipping_tax_rate: 0, + }); + }); + + it("when shipping line is taxable and tax is not included, returns calculated gross & net amounts", () => { + const taxableShippingLine = transformer.transform(TAX_EXCLUDED_SHIPPING_TRANSACTION_MOCK); + + expect(taxableShippingLine).toEqual({ + shipping_price_gross_amount: 84.87, + shipping_price_net_amount: 77.51, + shipping_tax_rate: 0, + }); + }); +}); diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-shipping-transformer.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-shipping-transformer.ts new file mode 100644 index 0000000..eb0f1aa --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-shipping-transformer.ts @@ -0,0 +1,59 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { numbers } from "../../taxes/numbers"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; +import { Response, SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter"; + +export class AvataxCalculateTaxesResponseShippingTransformer { + transform( + transaction: TransactionModel + ): Pick< + Response, + "shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate" + > { + const shippingLine = transaction.lines?.find((line) => line.itemCode === SHIPPING_ITEM_CODE); + + if (!shippingLine) { + return { + shipping_price_gross_amount: 0, + shipping_price_net_amount: 0, + shipping_tax_rate: 0, + }; + } + + if (!shippingLine.isItemTaxable) { + return { + shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow( + shippingLine.lineAmount, + new Error("shippingLine.lineAmount is undefined") + ), + shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow( + shippingLine.lineAmount, + new Error("shippingLine.lineAmount is undefined") + ), + /* + * avatax doesnt return combined tax rate + * // todo: calculate percentage tax rate + */ + shipping_tax_rate: 0, + }; + } + + const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow( + shippingLine.taxCalculated, + new Error("shippingLine.taxCalculated is undefined") + ); + const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow( + shippingLine.taxableAmount, + new Error("shippingLine.taxableAmount is undefined") + ); + const shippingGrossAmount = numbers.roundFloatToTwoDecimals( + shippingTaxableAmount + shippingTaxCalculated + ); + + return { + shipping_price_gross_amount: shippingGrossAmount, + shipping_price_net_amount: shippingTaxableAmount, + shipping_tax_rate: 0, + }; + } +} diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer.ts new file mode 100644 index 0000000..db1c59c --- /dev/null +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer.ts @@ -0,0 +1,19 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { Response } from "./avatax-calculate-taxes-adapter"; +import { AvataxCalculateTaxesResponseLinesTransformer } from "./avatax-calculate-taxes-response-lines-transformer"; +import { AvataxCalculateTaxesResponseShippingTransformer } from "./avatax-calculate-taxes-response-shipping-transformer"; + +export class AvataxCalculateTaxesResponseTransformer { + transform(response: TransactionModel): Response { + const shippingTransformer = new AvataxCalculateTaxesResponseShippingTransformer(); + const shipping = shippingTransformer.transform(response); + + const linesTransformer = new AvataxCalculateTaxesResponseLinesTransformer(); + const lines = linesTransformer.transform(response); + + return { + ...shipping, + lines, + }; + } +} diff --git a/apps/taxes/src/modules/avatax/maps/avatax-calculate-taxes-map.test.ts b/apps/taxes/src/modules/avatax/maps/avatax-calculate-taxes-map.test.ts deleted file mode 100644 index 0123944..0000000 --- a/apps/taxes/src/modules/avatax/maps/avatax-calculate-taxes-map.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - SHIPPING_ITEM_CODE, - mapPayloadLines, - mapResponseProductLines, - mapResponseShippingLine, -} from "./avatax-calculate-taxes-map"; -import { mapPayloadArgsMocks, transactionModelMocks } from "./mocks"; - -describe("avataxCalculateTaxesMaps", () => { - describe("mapResponseShippingLine", () => { - it("when shipping line is not present, returns 0s", () => { - const shippingLine = mapResponseShippingLine(transactionModelMocks.noShippingLine); - - expect(shippingLine).toEqual({ - shipping_price_gross_amount: 0, - shipping_price_net_amount: 0, - shipping_tax_rate: 0, - }); - }); - it("when shipping line is not taxable, returns line amount", () => { - const nonTaxableShippingLine = mapResponseShippingLine(transactionModelMocks.nonTaxable); - - expect(nonTaxableShippingLine).toEqual({ - shipping_price_gross_amount: 77.51, - shipping_price_net_amount: 77.51, - shipping_tax_rate: 0, - }); - }); - - it("when shipping line is taxable and tax is included, returns calculated gross & net amounts", () => { - const taxableShippingLine = mapResponseShippingLine( - transactionModelMocks.taxable.taxIncluded - ); - - expect(taxableShippingLine).toEqual({ - shipping_price_gross_amount: 77.51, - shipping_price_net_amount: 70.78, - shipping_tax_rate: 0, - }); - }); - - it("when shipping line is taxable and tax is not included, returns calculated gross & net amounts", () => { - const taxableShippingLine = mapResponseShippingLine( - transactionModelMocks.taxable.taxNotIncluded - ); - - expect(taxableShippingLine).toEqual({ - shipping_price_gross_amount: 84.87, - shipping_price_net_amount: 77.51, - shipping_tax_rate: 0, - }); - }); - }); - describe("mapResponseProductLines", () => { - it("when product lines are not taxable, returns line amount", () => { - const nonTaxableProductLines = mapResponseProductLines(transactionModelMocks.nonTaxable); - - expect(nonTaxableProductLines).toEqual([ - { - total_gross_amount: 20, - total_net_amount: 20, - tax_rate: 0, - }, - ]); - }); - - it("when product lines are taxable and tax is included, returns calculated gross & net amounts", () => { - const taxableProductLines = mapResponseProductLines( - transactionModelMocks.taxable.taxIncluded - ); - - expect(taxableProductLines).toEqual([ - { - total_gross_amount: 40, - total_net_amount: 36.53, - tax_rate: 0, - }, - ]); - }); - - it("when product lines are taxable and tax is not included, returns calculated gross & net amounts", () => { - const taxableProductLines = mapResponseProductLines( - transactionModelMocks.taxable.taxNotIncluded - ); - - expect(taxableProductLines).toEqual([ - { - total_gross_amount: 43.8, - total_net_amount: 40, - tax_rate: 0, - }, - ]); - }); - }); - describe.todo("mapPayload", () => { - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - }); - describe("mapLines", () => { - const lines = mapPayloadLines( - mapPayloadArgsMocks.default.taxBase, - mapPayloadArgsMocks.default.config - ); - - it("includes shipping as a line", () => { - expect(lines).toContainEqual({ - itemCode: SHIPPING_ITEM_CODE, - quantity: 1, - amount: 48.33, - taxCode: mapPayloadArgsMocks.default.config.shippingTaxCode, - taxIncluded: false, - }); - }); - - it("returns the correct quantity of individual lines", () => { - expect(lines).toContainEqual({ - quantity: 3, - amount: 252, - taxCode: "", - taxIncluded: false, - }); - }); - }); -}); diff --git a/apps/taxes/src/modules/avatax/maps/avatax-calculate-taxes-map.ts b/apps/taxes/src/modules/avatax/maps/avatax-calculate-taxes-map.ts deleted file mode 100644 index 6a46e7a..0000000 --- a/apps/taxes/src/modules/avatax/maps/avatax-calculate-taxes-map.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { LineItemModel } from "avatax/lib/models/LineItemModel"; -import { TransactionModel } from "avatax/lib/models/TransactionModel"; -import { TaxBaseFragment } from "../../../../generated/graphql"; - -import { DocumentType } from "avatax/lib/enums/DocumentType"; -import { ChannelConfig } from "../../channels-configuration/channels-config"; -import { numbers } from "../../taxes/numbers"; -import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook"; -import { CreateTransactionArgs } from "../avatax-client"; -import { AvataxConfig } from "../avatax-config"; -import { avataxAddressFactory } from "./address-factory"; -import { taxProviderUtils } from "../../taxes/tax-provider-utils"; - -/** - * * Shipping is a regular line item in Avatax - * https://developer.avalara.com/avatax/dev-guide/shipping-and-handling/taxability-of-shipping-charges/ - */ -export const SHIPPING_ITEM_CODE = "Shipping"; - -export function mapPayloadLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] { - const productLines = taxBase.lines.map((line) => ({ - amount: line.totalPrice.amount, - taxIncluded: taxBase.pricesEnteredWithTax, - // todo: get from tax code matcher - taxCode: "", - quantity: line.quantity, - })); - - if (taxBase.shippingPrice.amount !== 0) { - // * In Avatax, shipping is a regular line - const shippingLine: LineItemModel = { - amount: taxBase.shippingPrice.amount, - itemCode: SHIPPING_ITEM_CODE, - taxCode: config.shippingTaxCode, - quantity: 1, - taxIncluded: taxBase.pricesEnteredWithTax, - }; - - return [...productLines, shippingLine]; - } - - return productLines; -} - -export type AvataxCalculateTaxesMapPayloadArgs = { - taxBase: TaxBaseFragment; - channel: ChannelConfig; - config: AvataxConfig; -}; - -const mapPayload = (props: AvataxCalculateTaxesMapPayloadArgs): CreateTransactionArgs => { - const { taxBase, channel, config } = props; - - return { - model: { - type: DocumentType.SalesOrder, - customerCode: taxBase.sourceObject.user?.id ?? "", - companyCode: config.companyCode, - // * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit - commit: config.isAutocommit, - addresses: { - shipFrom: avataxAddressFactory.fromChannelAddress(channel.address), - shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!), - }, - currencyCode: taxBase.currency, - lines: mapPayloadLines(taxBase, config), - date: new Date(), - }, - }; -}; - -export function mapResponseShippingLine( - transaction: TransactionModel -): Pick< - CalculateTaxesResponse, - "shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate" -> { - const shippingLine = transaction.lines?.find((line) => line.itemCode === SHIPPING_ITEM_CODE); - - if (!shippingLine) { - return { - shipping_price_gross_amount: 0, - shipping_price_net_amount: 0, - shipping_tax_rate: 0, - }; - } - - if (!shippingLine.isItemTaxable) { - return { - shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow( - shippingLine.lineAmount, - new Error("shippingLine.lineAmount is undefined") - ), - shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow( - shippingLine.lineAmount, - new Error("shippingLine.lineAmount is undefined") - ), - /* - * avatax doesnt return combined tax rate - * // todo: calculate percentage tax rate - */ - shipping_tax_rate: 0, - }; - } - - const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow( - shippingLine.taxCalculated, - new Error("shippingLine.taxCalculated is undefined") - ); - const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow( - shippingLine.taxableAmount, - new Error("shippingLine.taxableAmount is undefined") - ); - const shippingGrossAmount = numbers.roundFloatToTwoDecimals( - shippingTaxableAmount + shippingTaxCalculated - ); - - return { - shipping_price_gross_amount: shippingGrossAmount, - shipping_price_net_amount: shippingTaxableAmount, - shipping_tax_rate: 0, - }; -} - -export function mapResponseProductLines( - transaction: TransactionModel -): CalculateTaxesResponse["lines"] { - const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE); - - return ( - productLines?.map((line) => { - if (!line.isItemTaxable) { - return { - total_gross_amount: taxProviderUtils.resolveOptionalOrThrow( - line.lineAmount, - new Error("line.lineAmount is undefined") - ), - total_net_amount: taxProviderUtils.resolveOptionalOrThrow( - line.lineAmount, - new Error("line.lineAmount is undefined") - ), - tax_rate: 0, - }; - } - - const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow( - line.taxCalculated, - new Error("line.taxCalculated is undefined") - ); - const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow( - line.taxableAmount, - new Error("line.taxableAmount is undefined") - ); - const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals( - lineTotalNetAmount + lineTaxCalculated - ); - - return { - total_gross_amount: lineTotalGrossAmount, - total_net_amount: lineTotalNetAmount, - /* - * avatax doesnt return combined tax rate - * // todo: calculate percentage tax rate - */ tax_rate: 0, - }; - }) ?? [] - ); -} - -const mapResponse = (transaction: TransactionModel): CalculateTaxesResponse => { - const shipping = mapResponseShippingLine(transaction); - - return { - ...shipping, - lines: mapResponseProductLines(transaction), - }; -}; - -export const avataxCalculateTaxesMaps = { - mapPayload, - mapResponse, -}; diff --git a/apps/taxes/src/modules/avatax/maps/avatax-order-created-map.test.ts b/apps/taxes/src/modules/avatax/maps/avatax-order-created-map.test.ts deleted file mode 100644 index caa9555..0000000 --- a/apps/taxes/src/modules/avatax/maps/avatax-order-created-map.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { OrderStatus } from "../../../../generated/graphql"; -import { - CreateTransactionMapPayloadArgs, - avataxOrderCreatedMaps, -} from "./avatax-order-created-map"; - -const MOCKED_ARGS: CreateTransactionMapPayloadArgs = { - order: { - id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5", - created: "2023-04-11T11:03:09.304109+00:00", - status: OrderStatus.Unfulfilled, - user: { - id: "VXNlcjo5ZjY3ZjY0Zi1iZjY5LTQ5ZjYtYjQ4Zi1iZjY3ZjY0ZjY0ZjY=", - email: "tester@saleor.io", - }, - channel: { - id: "Q2hhbm5lbDoy", - slug: "channel-pln", - }, - shippingAddress: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - countryArea: "CA", - postalCode: "90002", - country: { - code: "US", - }, - }, - billingAddress: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - countryArea: "CA", - postalCode: "90002", - country: { - code: "US", - }, - }, - total: { - net: { - amount: 183.33, - }, - tax: { - amount: 12.83, - }, - currency: "USD", - }, - shippingPrice: { - gross: { - amount: 48.33, - }, - net: { - amount: 43.74, - }, - }, - lines: [ - { - productSku: "328223581", - productName: "Monospace Tee", - quantity: 3, - unitPrice: { - net: { - amount: 90, - }, - }, - totalPrice: { - net: { - amount: 270, - }, - tax: { - amount: 8.55, - }, - }, - }, - { - productSku: "328223580", - productName: "Polyspace Tee", - quantity: 1, - unitPrice: { - net: { - amount: 45, - }, - }, - - totalPrice: { - net: { - amount: 45, - }, - tax: { - amount: 4.28, - }, - }, - }, - ], - discounts: [ - { - amount: { - amount: 10, - }, - id: "RGlzY291bnREaXNjb3VudDox", - }, - { - amount: { - amount: 21.45, - }, - id: "RGlzY291bnREaXNjb3VudDoy", - }, - ], - }, - channel: { - providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081", - enabled: false, - address: { - country: "US", - zip: "92093", - state: "CA", - city: "La Jolla", - street: "9500 Gilman Drive", - }, - }, - config: { - companyCode: "DEFAULT", - isAutocommit: true, - isSandbox: true, - name: "Avatax-1", - password: "user-password", - username: "user-name", - shippingTaxCode: "FR000000", - }, -}; - -describe("avataxOrderCreatedMaps", () => { - describe.todo("mapResponse", () => { - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - }); - describe("mapPayload", () => { - it("returns lines with discounted: true when there are discounts", () => { - const payload = avataxOrderCreatedMaps.mapPayload(MOCKED_ARGS); - - const linesWithoutShipping = payload.model.lines.slice(0, -1); - const check = linesWithoutShipping.every((line) => line.discounted === true); - - expect(check).toBeTruthy(); - }); - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - }); - describe("mapLines", () => { - const lines = avataxOrderCreatedMaps.mapLines(MOCKED_ARGS.order, MOCKED_ARGS.config); - - it("returns the correct number of lines", () => { - expect(lines).toHaveLength(3); - }); - - it("includes shipping as a line", () => { - expect(lines).toContainEqual({ - itemCode: avataxOrderCreatedMaps.consts.shippingItemCode, - taxCode: MOCKED_ARGS.config.shippingTaxCode, - quantity: 1, - amount: 48.33, - taxIncluded: true, - }); - }); - - it("includes products as lines", () => { - const [first, second] = lines; - - expect(first).toContain({ - itemCode: "328223581", - description: "Monospace Tee", - quantity: 3, - amount: 278.55, - }); - expect(second).toContain({ - itemCode: "328223580", - description: "Polyspace Tee", - quantity: 1, - amount: 49.28, - }); - }); - }); - describe("mapDiscounts", () => { - it("sums up all discounts", () => { - const discounts = avataxOrderCreatedMaps.mapDiscounts(MOCKED_ARGS.order.discounts); - - expect(discounts).toEqual(31.45); - }); - - it("returns 0 if there are no discounts", () => { - const discounts = avataxOrderCreatedMaps.mapDiscounts([]); - - expect(discounts).toEqual(0); - }); - }); -}); diff --git a/apps/taxes/src/modules/avatax/maps/avatax-order-created-map.ts b/apps/taxes/src/modules/avatax/maps/avatax-order-created-map.ts deleted file mode 100644 index 06c27c6..0000000 --- a/apps/taxes/src/modules/avatax/maps/avatax-order-created-map.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DocumentType } from "avatax/lib/enums/DocumentType"; -import { LineItemModel } from "avatax/lib/models/LineItemModel"; -import { TransactionModel } from "avatax/lib/models/TransactionModel"; -import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; -import { ChannelConfig } from "../../channels-configuration/channels-config"; -import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; -import { CreateTransactionArgs } from "../avatax-client"; -import { AvataxConfig } from "../avatax-config"; -import { avataxAddressFactory } from "./address-factory"; -import { numbers } from "../../taxes/numbers"; - -/** - * * Shipping is a regular line item in Avatax - * https://developer.avalara.com/avatax/dev-guide/shipping-and-handling/taxability-of-shipping-charges/ - */ -const SHIPPING_ITEM_CODE = "Shipping"; - -function mapLines(order: OrderCreatedSubscriptionFragment, config: AvataxConfig): LineItemModel[] { - const productLines: LineItemModel[] = order.lines.map((line) => ({ - taxIncluded: true, - amount: numbers.roundFloatToTwoDecimals( - line.totalPrice.net.amount + line.totalPrice.tax.amount - ), - // todo: get from tax code matcher - taxCode: "", - quantity: line.quantity, - description: line.productName, - itemCode: line.productSku ?? "", - discounted: order.discounts.length > 0, - })); - - if (order.shippingPrice.net.amount !== 0) { - // * In Avatax, shipping is a regular line - const shippingLine: LineItemModel = { - amount: order.shippingPrice.gross.amount, - taxIncluded: true, - itemCode: SHIPPING_ITEM_CODE, - /** - * * Different shipping methods can have different tax codes. - * https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\ - */ - taxCode: config.shippingTaxCode, - quantity: 1, - }; - - return [...productLines, shippingLine]; - } - - return productLines; -} - -function mapDiscounts(discounts: OrderCreatedSubscriptionFragment["discounts"]): number { - return discounts.reduce((total, current) => total + Number(current.amount.amount), 0); -} - -export type CreateTransactionMapPayloadArgs = { - order: OrderCreatedSubscriptionFragment; - channel: ChannelConfig; - config: AvataxConfig; -}; - -const mapPayload = ({ - order, - channel, - config, -}: CreateTransactionMapPayloadArgs): CreateTransactionArgs => { - return { - model: { - type: DocumentType.SalesInvoice, - customerCode: - order.user?.id ?? - "" /* In Saleor Avatax plugin, the customer code is 0. In Taxes App, we set it to the user id. */, - companyCode: config.companyCode, - // * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit - commit: config.isAutocommit, - addresses: { - shipFrom: avataxAddressFactory.fromChannelAddress(channel.address), - // billing or shipping address? - shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!), - }, - currencyCode: order.total.currency, - email: order.user?.email ?? "", - lines: mapLines(order, config), - date: new Date(order.created), - discount: mapDiscounts(order.discounts), - }, - }; -}; - -const mapResponse = (response: TransactionModel): CreateOrderResponse => { - return { - id: response.code ?? "", - }; -}; - -export const avataxOrderCreatedMaps = { - mapPayload, - mapResponse, - mapLines, - mapDiscounts, - consts: { - shippingItemCode: SHIPPING_ITEM_CODE, - }, -}; diff --git a/apps/taxes/src/modules/avatax/maps/avatax-order-fulfilled-map.test.ts b/apps/taxes/src/modules/avatax/maps/avatax-order-fulfilled-map.test.ts deleted file mode 100644 index c2df46d..0000000 --- a/apps/taxes/src/modules/avatax/maps/avatax-order-fulfilled-map.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - CommitTransactionMapPayloadArgs, - avataxOrderFulfilledMaps, -} from "./avatax-order-fulfilled-map"; -import { OrderFulfilledSubscriptionFragment, OrderStatus } from "../../../../generated/graphql"; -import { DocumentType } from "avatax/lib/enums/DocumentType"; - -const MOCKED_METADATA: OrderFulfilledSubscriptionFragment["privateMetadata"] = [ - { - key: avataxOrderFulfilledMaps.providerOrderIdKey, - value: "transaction-code", - }, -]; - -const MOCKED_MAP_PAYLOAD_ARGS: CommitTransactionMapPayloadArgs = { - order: { - id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5", - created: "2023-04-11T11:03:09.304109+00:00", - privateMetadata: MOCKED_METADATA, - channel: { - id: "Q2hhbm5lbDoy", - slug: "channel-pln", - }, - shippingAddress: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - countryArea: "CA", - postalCode: "90002", - country: { - code: "US", - }, - }, - billingAddress: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - countryArea: "CA", - postalCode: "90002", - country: { - code: "US", - }, - }, - total: { - net: { - amount: 183.33, - }, - tax: { - amount: 12.83, - }, - }, - shippingPrice: { - net: { - amount: 48.33, - }, - }, - lines: [ - { - productSku: "328223581", - productName: "Monospace Tee", - quantity: 1, - unitPrice: { - net: { - amount: 90, - }, - }, - totalPrice: { - net: { - amount: 90, - }, - tax: { - amount: 8.55, - }, - }, - }, - { - productSku: "328223580", - productName: "Polyspace Tee", - quantity: 1, - unitPrice: { - net: { - amount: 45, - }, - }, - totalPrice: { - net: { - amount: 45, - }, - tax: { - amount: 4.28, - }, - }, - }, - ], - }, - config: { - companyCode: "DEFAULT", - isAutocommit: true, - isSandbox: true, - name: "Avatax-1", - password: "user-password", - username: "user-name", - shippingTaxCode: "FR000000", - }, -}; - -describe("avataxOrderFulfilledMaps", () => { - describe("getTransactionCodeFromMetadata", () => { - it("should return transaction code", () => { - expect(avataxOrderFulfilledMaps.getTransactionCodeFromMetadata(MOCKED_METADATA)).toBe( - "transaction-code" - ); - }); - - it("should throw error when transaction code not found", () => { - expect(() => avataxOrderFulfilledMaps.getTransactionCodeFromMetadata([])).toThrowError(); - }); - }); - describe("mapPayload", () => { - it("should return mapped payload", () => { - const mappedPayload = avataxOrderFulfilledMaps.mapPayload(MOCKED_MAP_PAYLOAD_ARGS); - - expect(mappedPayload).toEqual({ - transactionCode: "transaction-code", - companyCode: "DEFAULT", - documentType: DocumentType.SalesInvoice, - model: { - commit: true, - }, - }); - }); - }); -}); diff --git a/apps/taxes/src/modules/avatax/maps/avatax-order-fulfilled-map.ts b/apps/taxes/src/modules/avatax/maps/avatax-order-fulfilled-map.ts deleted file mode 100644 index b0e9b8d..0000000 --- a/apps/taxes/src/modules/avatax/maps/avatax-order-fulfilled-map.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DocumentType } from "avatax/lib/enums/DocumentType"; -import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql"; - -import { CommitTransactionArgs } from "../avatax-client"; -import { AvataxConfig } from "../avatax-config"; - -// * This is the key that we use to store the provider order id in the Saleor order metadata. -export const PROVIDER_ORDER_ID_KEY = "externalId"; - -function getTransactionCodeFromMetadata( - metadata: OrderFulfilledSubscriptionFragment["privateMetadata"] -) { - const transactionCode = metadata.find((item) => item.key === PROVIDER_ORDER_ID_KEY); - - if (!transactionCode) { - throw new Error("Transaction code not found"); - } - - return transactionCode.value; -} - -export type CommitTransactionMapPayloadArgs = { - order: OrderFulfilledSubscriptionFragment; - config: AvataxConfig; -}; - -const mapPayload = ({ order, config }: CommitTransactionMapPayloadArgs): CommitTransactionArgs => { - const transactionCode = getTransactionCodeFromMetadata(order.privateMetadata); - - return { - transactionCode, - companyCode: config.companyCode ?? "", - documentType: DocumentType.SalesInvoice, - model: { - commit: true, - }, - }; -}; - -export const avataxOrderFulfilledMaps = { - mapPayload, - getTransactionCodeFromMetadata, - providerOrderIdKey: PROVIDER_ORDER_ID_KEY, -}; diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-adapter.test.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-adapter.test.ts new file mode 100644 index 0000000..ac4952d --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-adapter.test.ts @@ -0,0 +1,6 @@ +import { describe, it } from "vitest"; + +describe("AvataxOrderCreatedAdapter", () => { + // ? how to mock internal call to avatax? + it.todo("calls avatax client", () => {}); +}); diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-adapter.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-adapter.ts new file mode 100644 index 0000000..1e25064 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-adapter.ts @@ -0,0 +1,44 @@ +import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; +import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; +import { AvataxClient, CreateTransactionArgs } from "../avatax-client"; +import { AvataxConfig } from "../avatax-config"; +import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer"; +import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer"; +import { Logger, createLogger } from "../../../lib/logger"; + +export type Payload = { + order: OrderCreatedSubscriptionFragment; + channelConfig: ChannelConfig; + config: AvataxConfig; +}; +export type Target = CreateTransactionArgs; +type Response = CreateOrderResponse; + +export class AvataxOrderCreatedAdapter implements WebhookAdapter { + private logger: Logger; + + constructor(private readonly config: AvataxConfig) { + this.logger = createLogger({ service: "AvataxOrderCreatedAdapter" }); + } + + async send(payload: Pick): Promise { + this.logger.debug({ payload }, "send called with:"); + + const payloadTransformer = new AvataxOrderCreatedPayloadTransformer(); + const target = payloadTransformer.transform({ ...payload, config: this.config }); + + const client = new AvataxClient(this.config); + const response = await client.createTransaction(target); + + this.logger.debug({ response }, "Avatax createTransaction response:"); + + const responseTransformer = new AvataxOrderCreatedResponseTransformer(); + const transformedResponse = responseTransformer.transform(response); + + this.logger.debug({ transformedResponse }, "Transformed Avatax createTransaction response to:"); + + return transformedResponse; + } +} diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-mock-generator.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-mock-generator.ts new file mode 100644 index 0000000..8e3a332 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-mock-generator.ts @@ -0,0 +1,70 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { orderCreatedTransactionMock } from "./avatax-order-created-response-transaction-mock"; +import { AvataxConfig } from "../avatax-config"; +import { defaultOrder } from "../../../mocks"; + +const defaultChannelConfig: ChannelConfig = { + providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50", + enabled: false, + address: { + country: "US", + zip: "95008", + state: "CA", + city: "Campbell", + street: "33 N. First Street", + }, +}; + +const defaultOrderCreatedResponse: TransactionModel = orderCreatedTransactionMock; + +const defaultAvataxConfig: AvataxConfig = { + companyCode: "DEFAULT", + isAutocommit: false, + isSandbox: true, + name: "Avatax-1", + password: "password", + username: "username", + shippingTaxCode: "FR000000", +}; + +const testingScenariosMap = { + default: { + order: defaultOrder, + channelConfig: defaultChannelConfig, + response: defaultOrderCreatedResponse, + avataxConfig: defaultAvataxConfig, + }, +}; + +type TestingScenario = keyof typeof testingScenariosMap; + +export class AvataxOrderCreatedMockGenerator { + constructor(private scenario: TestingScenario = "default") {} + generateOrder = ( + overrides: Partial = {} + ): OrderCreatedSubscriptionFragment => + structuredClone({ + ...testingScenariosMap[this.scenario].order, + ...overrides, + }); + + generateChannelConfig = (overrides: Partial = {}): ChannelConfig => + structuredClone({ + ...testingScenariosMap[this.scenario].channelConfig, + ...overrides, + }); + + generateAvataxConfig = (overrides: Partial = {}): AvataxConfig => + structuredClone({ + ...testingScenariosMap[this.scenario].avataxConfig, + ...overrides, + }); + + generateResponse = (overrides: Partial = {}): TransactionModel => + structuredClone({ + ...testingScenariosMap[this.scenario].response, + ...overrides, + }); +} diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-payload-transformer.test.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-payload-transformer.test.ts new file mode 100644 index 0000000..4d48522 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-payload-transformer.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; +import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator"; +import { + AvataxOrderCreatedPayloadTransformer, + mapLines, +} from "./avatax-order-created-payload-transformer"; + +const mockGenerator = new AvataxOrderCreatedMockGenerator(); +const orderMock = mockGenerator.generateOrder(); +const discountedOrderMock = mockGenerator.generateOrder({ + discounts: [ + { + amount: { + amount: 10, + }, + id: "RGlzY291bnREaXNjb3VudDox", + }, + ], +}); +const avataxConfigMock = mockGenerator.generateAvataxConfig(); +const channelConfigMock = mockGenerator.generateChannelConfig(); + +describe("AvataxOrderCreatedPayloadTransformer", () => { + it("returns lines with discounted: true when there are discounts", () => { + const transformer = new AvataxOrderCreatedPayloadTransformer(); + const payloadMock = { + order: discountedOrderMock, + config: avataxConfigMock, + channelConfig: channelConfigMock, + }; + + const payload = transformer.transform(payloadMock); + + const linesWithoutShipping = payload.model.lines.slice(0, -1); + const check = linesWithoutShipping.every((line) => line.discounted === true); + + expect(check).toBe(true); + }); + it("returns lines with discounted: false when there are no discounts", () => { + const transformer = new AvataxOrderCreatedPayloadTransformer(); + const payloadMock = { + order: orderMock, + config: avataxConfigMock, + channelConfig: channelConfigMock, + }; + + const payload = transformer.transform(payloadMock); + + const linesWithoutShipping = payload.model.lines.slice(0, -1); + const check = linesWithoutShipping.every((line) => line.discounted === false); + + expect(check).toBe(true); + }); +}); + +describe("mapLines", () => { + const lines = mapLines(orderMock, avataxConfigMock); + + it("returns the correct number of lines", () => { + expect(lines).toHaveLength(4); + }); + + it("includes shipping as a line", () => { + expect(lines).toContainEqual({ + itemCode: "Shipping", + taxCode: "FR000000", + quantity: 1, + amount: 59.17, + taxIncluded: true, + }); + }); + + it("includes products as lines", () => { + const [first, second, third] = lines; + + expect(first).toContain({ + itemCode: "328223580", + description: "Monospace Tee", + quantity: 3, + amount: 65.18, + }); + expect(second).toContain({ + itemCode: "328223581", + description: "Monospace Tee", + quantity: 1, + amount: 21.73, + }); + expect(third).toContain({ + itemCode: "118223581", + description: "Paul's Balance 420", + quantity: 2, + amount: 108.63, + }); + }); +}); diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-payload-transformer.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-payload-transformer.ts new file mode 100644 index 0000000..1666697 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-payload-transformer.ts @@ -0,0 +1,77 @@ +import { LineItemModel } from "avatax/lib/models/LineItemModel"; +import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; +import { numbers } from "../../taxes/numbers"; +import { AvataxConfig } from "../avatax-config"; +import { avataxAddressFactory } from "../address-factory"; +import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { Payload, Target } from "./avatax-order-created-adapter"; +import { discountUtils } from "../../taxes/discount-utils"; + +const SHIPPING_ITEM_CODE = "Shipping"; + +// ? separate class? +export function mapLines( + order: OrderCreatedSubscriptionFragment, + config: AvataxConfig +): LineItemModel[] { + const productLines: LineItemModel[] = order.lines.map((line) => ({ + // taxes are included because we treat what is passed in payload as the source of truth + taxIncluded: true, + amount: numbers.roundFloatToTwoDecimals( + line.totalPrice.net.amount + line.totalPrice.tax.amount + ), + // todo: get from tax code matcher + taxCode: "", + quantity: line.quantity, + description: line.productName, + itemCode: line.productSku ?? "", + discounted: order.discounts.length > 0, + })); + + if (order.shippingPrice.net.amount !== 0) { + // * In Avatax, shipping is a regular line + const shippingLine: LineItemModel = { + amount: order.shippingPrice.gross.amount, + taxIncluded: true, + itemCode: SHIPPING_ITEM_CODE, + /** + * * Different shipping methods can have different tax codes. + * https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\ + */ + taxCode: config.shippingTaxCode, + quantity: 1, + }; + + return [...productLines, shippingLine]; + } + + return productLines; +} + +export class AvataxOrderCreatedPayloadTransformer { + transform = ({ order, channelConfig, config }: Payload): Target => { + return { + model: { + type: DocumentType.SalesInvoice, + customerCode: + order.user?.id ?? + "" /* In Saleor Avatax plugin, the customer code is 0. In Taxes App, we set it to the user id. */, + companyCode: config.companyCode, + // * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit + commit: config.isAutocommit, + addresses: { + shipFrom: avataxAddressFactory.fromChannelAddress(channelConfig.address), + // billing or shipping address? + shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!), + }, + currencyCode: order.total.currency, + email: order.user?.email ?? "", + lines: mapLines(order, config), + date: new Date(order.created), + discount: discountUtils.sumDiscounts( + order.discounts.map((discount) => discount.amount.amount) + ), + }, + }; + }; +} diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transaction-mock.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transaction-mock.ts new file mode 100644 index 0000000..3affc97 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transaction-mock.ts @@ -0,0 +1,1260 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; + +export const orderCreatedTransactionMock = { + id: 85026453274669, + code: "31fa9e94-e8df-429b-b303-549e7bc10f1e", + companyId: 7799660, + date: "2023-05-25", + status: "Saved", + type: "SalesInvoice", + batchCode: "", + currencyCode: "USD", + exchangeRateCurrencyCode: "USD", + customerUsageType: "", + entityUseCode: "", + customerVendorCode: "VXNlcjoyMDg0NTEwNDEw", + customerCode: "VXNlcjoyMDg0NTEwNDEw", + exemptNo: "", + reconciled: false, + locationCode: "", + reportingLocationCode: "", + purchaseOrderNo: "", + referenceCode: "", + salespersonCode: "", + taxOverrideType: "None", + taxOverrideAmount: 0, + taxOverrideReason: "", + totalAmount: 137.34, + totalExempt: 0, + totalDiscount: 0, + totalTax: 11.83, + totalTaxable: 137.34, + totalTaxCalculated: 11.83, + adjustmentReason: "NotAdjusted", + adjustmentDescription: "", + locked: false, + region: "CA", + country: "US", + version: 1, + softwareVersion: "23.4.0.0", + originAddressId: 85026453274671, + destinationAddressId: 85026453274670, + exchangeRateEffectiveDate: "2023-05-25", + exchangeRate: 1, + description: "", + email: "happy.customer@saleor.io", + businessIdentificationNo: "", + modifiedDate: "2023-05-25T11:11:17.5517302Z", + modifiedUserId: 6479978, + taxDate: "2023-05-25", + lines: [ + { + id: 85026453274675, + transactionId: 85026453274669, + lineNumber: "1", + boundaryOverrideId: 0, + customerUsageType: "", + entityUseCode: "", + description: "Monospace Tee", + destinationAddressId: 85026453274670, + originAddressId: 85026453274671, + discountAmount: 0, + discountTypeId: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + isSSTP: false, + itemCode: "328223580", + lineAmount: 18.42, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: "2023-05-25", + revAccount: "", + sourcing: "Mixed", + tax: 1.58, + taxableAmount: 18.42, + taxCalculated: 1.58, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: "2023-05-25", + taxEngine: "", + taxOverrideType: "None", + businessIdentificationNo: "", + taxOverrideAmount: 0, + taxOverrideReason: "", + taxIncluded: true, + details: [ + { + id: 85026453274685, + transactionLineId: 85026453274675, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "06", + jurisName: "CALIFORNIA", + jurisdictionId: 5000531, + signatureCode: "AGAM", + stateAssignedNo: "", + jurisType: "STA", + jurisdictionType: "State", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.06, + rateRuleId: 1343583, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 1.1, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 1.1, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 1.1, + reportingTaxCalculated: 1.1, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274686, + transactionLineId: 85026453274675, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "085", + jurisName: "SANTA CLARA", + jurisdictionId: 280, + signatureCode: "AJBI", + stateAssignedNo: "", + jurisType: "CTY", + jurisdictionType: "County", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.0025, + rateRuleId: 1358122, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.05, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.05, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.05, + reportingTaxCalculated: 0.05, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274687, + transactionLineId: 85026453274675, + transactionId: 85026453274669, + addressId: 85026453274670, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + jurisdictionId: 2001061430, + signatureCode: "EMBE", + stateAssignedNo: "940", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01375, + rateRuleId: 2443104, + rateSourceId: 3, + serCode: "", + sourcing: "Destination", + tax: 0.25, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 4024330, + taxCalculated: 0.25, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.25, + reportingTaxCalculated: 0.25, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274688, + transactionLineId: 85026453274675, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + jurisdictionId: 2001061797, + signatureCode: "EMUA", + stateAssignedNo: "43", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01, + rateRuleId: 1316507, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.18, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.18, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.18, + reportingTaxCalculated: 0.18, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + ], + nonPassthroughDetails: [], + lineLocationTypes: [ + { + documentLineLocationTypeId: 85026453274680, + documentLineId: 85026453274675, + documentAddressId: 85026453274671, + locationTypeCode: "ShipFrom", + }, + { + documentLineLocationTypeId: 85026453274681, + documentLineId: 85026453274675, + documentAddressId: 85026453274670, + locationTypeCode: "ShipTo", + }, + ], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 85026453274676, + transactionId: 85026453274669, + lineNumber: "2", + boundaryOverrideId: 0, + customerUsageType: "", + entityUseCode: "", + description: "Monospace Tee", + destinationAddressId: 85026453274670, + originAddressId: 85026453274671, + discountAmount: 0, + discountTypeId: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + isSSTP: false, + itemCode: "328223581", + lineAmount: 18.42, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: "2023-05-25", + revAccount: "", + sourcing: "Mixed", + tax: 1.58, + taxableAmount: 18.42, + taxCalculated: 1.58, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: "2023-05-25", + taxEngine: "", + taxOverrideType: "None", + businessIdentificationNo: "", + taxOverrideAmount: 0, + taxOverrideReason: "", + taxIncluded: true, + details: [ + { + id: 85026453274695, + transactionLineId: 85026453274676, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "06", + jurisName: "CALIFORNIA", + jurisdictionId: 5000531, + signatureCode: "AGAM", + stateAssignedNo: "", + jurisType: "STA", + jurisdictionType: "State", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.06, + rateRuleId: 1343583, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 1.1, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 1.1, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 1.1, + reportingTaxCalculated: 1.1, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274696, + transactionLineId: 85026453274676, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "085", + jurisName: "SANTA CLARA", + jurisdictionId: 280, + signatureCode: "AJBI", + stateAssignedNo: "", + jurisType: "CTY", + jurisdictionType: "County", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.0025, + rateRuleId: 1358122, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.05, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.05, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.05, + reportingTaxCalculated: 0.05, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274697, + transactionLineId: 85026453274676, + transactionId: 85026453274669, + addressId: 85026453274670, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + jurisdictionId: 2001061430, + signatureCode: "EMBE", + stateAssignedNo: "940", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01375, + rateRuleId: 2443104, + rateSourceId: 3, + serCode: "", + sourcing: "Destination", + tax: 0.25, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 4024330, + taxCalculated: 0.25, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.25, + reportingTaxCalculated: 0.25, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274698, + transactionLineId: 85026453274676, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + jurisdictionId: 2001061797, + signatureCode: "EMUA", + stateAssignedNo: "43", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01, + rateRuleId: 1316507, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.18, + taxableAmount: 18.42, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.18, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 18.42, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 18.42, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.18, + reportingTaxCalculated: 0.18, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + ], + nonPassthroughDetails: [], + lineLocationTypes: [ + { + documentLineLocationTypeId: 85026453274690, + documentLineId: 85026453274676, + documentAddressId: 85026453274671, + locationTypeCode: "ShipFrom", + }, + { + documentLineLocationTypeId: 85026453274691, + documentLineId: 85026453274676, + documentAddressId: 85026453274670, + locationTypeCode: "ShipTo", + }, + ], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 85026453274677, + transactionId: 85026453274669, + lineNumber: "3", + boundaryOverrideId: 0, + customerUsageType: "", + entityUseCode: "", + description: "Paul's Balance 420", + destinationAddressId: 85026453274670, + originAddressId: 85026453274671, + discountAmount: 0, + discountTypeId: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + isSSTP: false, + itemCode: "118223581", + lineAmount: 46.03, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: "2023-05-25", + revAccount: "", + sourcing: "Mixed", + tax: 3.97, + taxableAmount: 46.03, + taxCalculated: 3.97, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: "2023-05-25", + taxEngine: "", + taxOverrideType: "None", + businessIdentificationNo: "", + taxOverrideAmount: 0, + taxOverrideReason: "", + taxIncluded: true, + details: [ + { + id: 85026453274705, + transactionLineId: 85026453274677, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "06", + jurisName: "CALIFORNIA", + jurisdictionId: 5000531, + signatureCode: "AGAM", + stateAssignedNo: "", + jurisType: "STA", + jurisdictionType: "State", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.06, + rateRuleId: 1343583, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 2.76, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 2.76, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 46.03, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.76, + reportingTaxCalculated: 2.76, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274706, + transactionLineId: 85026453274677, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "085", + jurisName: "SANTA CLARA", + jurisdictionId: 280, + signatureCode: "AJBI", + stateAssignedNo: "", + jurisType: "CTY", + jurisdictionType: "County", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.0025, + rateRuleId: 1358122, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.12, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.12, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 46.03, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.12, + reportingTaxCalculated: 0.12, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274707, + transactionLineId: 85026453274677, + transactionId: 85026453274669, + addressId: 85026453274670, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + jurisdictionId: 2001061430, + signatureCode: "EMBE", + stateAssignedNo: "940", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01375, + rateRuleId: 2443104, + rateSourceId: 3, + serCode: "", + sourcing: "Destination", + tax: 0.63, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 4024330, + taxCalculated: 0.63, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 46.03, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.63, + reportingTaxCalculated: 0.63, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274708, + transactionLineId: 85026453274677, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + jurisdictionId: 2001061797, + signatureCode: "EMUA", + stateAssignedNo: "43", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01, + rateRuleId: 1316507, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.46, + taxableAmount: 46.03, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.46, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 46.03, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 46.03, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.46, + reportingTaxCalculated: 0.46, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + ], + nonPassthroughDetails: [], + lineLocationTypes: [ + { + documentLineLocationTypeId: 85026453274700, + documentLineId: 85026453274677, + documentAddressId: 85026453274671, + locationTypeCode: "ShipFrom", + }, + { + documentLineLocationTypeId: 85026453274701, + documentLineId: 85026453274677, + documentAddressId: 85026453274670, + locationTypeCode: "ShipTo", + }, + ], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 85026453274678, + transactionId: 85026453274669, + lineNumber: "4", + boundaryOverrideId: 0, + customerUsageType: "", + entityUseCode: "", + description: "", + destinationAddressId: 85026453274670, + originAddressId: 85026453274671, + discountAmount: 0, + discountTypeId: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + isSSTP: false, + itemCode: "Shipping", + lineAmount: 54.47, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: "2023-05-25", + revAccount: "", + sourcing: "Mixed", + tax: 4.7, + taxableAmount: 54.47, + taxCalculated: 4.7, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: "2023-05-25", + taxEngine: "", + taxOverrideType: "None", + businessIdentificationNo: "", + taxOverrideAmount: 0, + taxOverrideReason: "", + taxIncluded: true, + details: [ + { + id: 85026453274715, + transactionLineId: 85026453274678, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "06", + jurisName: "CALIFORNIA", + jurisdictionId: 5000531, + signatureCode: "AGAM", + stateAssignedNo: "", + jurisType: "STA", + jurisdictionType: "State", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.06, + rateRuleId: 1343583, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 3.27, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 3.27, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 54.47, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 3.27, + reportingTaxCalculated: 3.27, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274716, + transactionLineId: 85026453274678, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "085", + jurisName: "SANTA CLARA", + jurisdictionId: 280, + signatureCode: "AJBI", + stateAssignedNo: "", + jurisType: "CTY", + jurisdictionType: "County", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.0025, + rateRuleId: 1358122, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.14, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.14, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 54.47, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.14, + reportingTaxCalculated: 0.14, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274717, + transactionLineId: 85026453274678, + transactionId: 85026453274669, + addressId: 85026453274670, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + jurisdictionId: 2001061430, + signatureCode: "EMBE", + stateAssignedNo: "940", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01375, + rateRuleId: 2443104, + rateSourceId: 3, + serCode: "", + sourcing: "Destination", + tax: 0.75, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 4024330, + taxCalculated: 0.75, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 54.47, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.75, + reportingTaxCalculated: 0.75, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + { + id: 85026453274718, + transactionLineId: 85026453274678, + transactionId: 85026453274669, + addressId: 85026453274671, + country: "US", + region: "CA", + countyFIPS: "", + stateFIPS: "", + exemptAmount: 0, + exemptReasonId: 4, + inState: true, + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + jurisdictionId: 2001061797, + signatureCode: "EMUA", + stateAssignedNo: "43", + jurisType: "STJ", + jurisdictionType: "Special", + nonTaxableAmount: 0, + nonTaxableRuleId: 0, + nonTaxableType: "RateRule", + rate: 0.01, + rateRuleId: 1316507, + rateSourceId: 3, + serCode: "", + sourcing: "Origin", + tax: 0.54, + taxableAmount: 54.47, + taxType: "Sales", + taxSubTypeId: "S", + taxTypeGroupId: "SalesAndUse", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxRegionId: 2128577, + taxCalculated: 0.54, + taxOverride: 0, + rateType: "General", + rateTypeCode: "G", + taxableUnits: 54.47, + nonTaxableUnits: 0, + exemptUnits: 0, + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 54.47, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.54, + reportingTaxCalculated: 0.54, + liabilityType: "Seller", + chargedTo: "Buyer", + }, + ], + nonPassthroughDetails: [], + lineLocationTypes: [ + { + documentLineLocationTypeId: 85026453274710, + documentLineId: 85026453274678, + documentAddressId: 85026453274671, + locationTypeCode: "ShipFrom", + }, + { + documentLineLocationTypeId: 85026453274711, + documentLineId: 85026453274678, + documentAddressId: 85026453274670, + locationTypeCode: "ShipTo", + }, + ], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + ], + addresses: [ + { + id: 85026453274670, + transactionId: 85026453274669, + boundaryLevel: "Address", + line1: "600 Montgomery St", + line2: "", + line3: "", + city: "San Francisco", + region: "CA", + postalCode: "94111-2702", + country: "US", + taxRegionId: 4024330, + latitude: "37.795255", + longitude: "-122.40313", + }, + { + id: 85026453274671, + transactionId: 85026453274669, + boundaryLevel: "Address", + line1: "33 N 1st St", + line2: "", + line3: "", + city: "Campbell", + region: "CA", + postalCode: "95008-2050", + country: "US", + taxRegionId: 2128577, + latitude: "37.287589", + longitude: "-121.944955", + }, + ], + locationTypes: [ + { + documentLocationTypeId: 85026453274673, + documentId: 85026453274669, + documentAddressId: 85026453274671, + locationTypeCode: "ShipFrom", + }, + { + documentLocationTypeId: 85026453274674, + documentId: 85026453274669, + documentAddressId: 85026453274670, + locationTypeCode: "ShipTo", + }, + ], + summary: [ + { + country: "US", + region: "CA", + jurisType: "State", + jurisCode: "06", + jurisName: "CALIFORNIA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Sales", + taxSubType: "S", + taxName: "CA STATE TAX", + rateType: "General", + taxable: 137.34, + rate: 0.06, + tax: 8.23, + taxCalculated: 8.23, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: "County", + jurisCode: "085", + jurisName: "SANTA CLARA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Sales", + taxSubType: "S", + taxName: "CA COUNTY TAX", + rateType: "General", + taxable: 137.34, + rate: 0.0025, + tax: 0.36, + taxCalculated: 0.36, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: "Special", + jurisCode: "EMBE0", + jurisName: "SAN FRANCISCO COUNTY DISTRICT TAX SP", + taxAuthorityType: 45, + stateAssignedNo: "940", + taxType: "Sales", + taxSubType: "S", + taxName: "CA SPECIAL TAX", + rateType: "General", + taxable: 137.34, + rate: 0.01375, + tax: 1.88, + taxCalculated: 1.88, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: "Special", + jurisCode: "EMUA0", + jurisName: "SANTA CLARA CO LOCAL TAX SL", + taxAuthorityType: 45, + stateAssignedNo: "43", + taxType: "Sales", + taxSubType: "S", + taxName: "CA SPECIAL TAX", + rateType: "General", + taxable: 137.34, + rate: 0.01, + tax: 1.36, + taxCalculated: 1.36, + nonTaxable: 0, + exemption: 0, + }, + ], +} as unknown as TransactionModel; diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transformer.test.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transformer.test.ts new file mode 100644 index 0000000..96b87ca --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transformer.test.ts @@ -0,0 +1,514 @@ +import { AdjustmentReason } from "avatax/lib/enums/AdjustmentReason"; +import { BoundaryLevel } from "avatax/lib/enums/BoundaryLevel"; +import { ChargedTo } from "avatax/lib/enums/ChargedTo"; +import { DocumentStatus } from "avatax/lib/enums/DocumentStatus"; +import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { JurisTypeId } from "avatax/lib/enums/JurisTypeId"; +import { JurisdictionType } from "avatax/lib/enums/JurisdictionType"; +import { LiabilityType } from "avatax/lib/enums/LiabilityType"; +import { RateType } from "avatax/lib/enums/RateType"; +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { describe, expect, it } from "vitest"; +import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer"; + +const MOCKED_TRANSACTION: TransactionModel = { + id: 0, + code: "8fc875ce-a929-4556-9f30-0165b1597d9f", + companyId: 7799640, + date: new Date(), + paymentDate: new Date(), + status: DocumentStatus.Temporary, + type: DocumentType.SalesOrder, + batchCode: "", + currencyCode: "USD", + exchangeRateCurrencyCode: "USD", + customerUsageType: "", + entityUseCode: "", + customerVendorCode: "VXNlcjoyMDg0NTEwNDEw", + customerCode: "VXNlcjoyMDg0NTEwNDEw", + exemptNo: "", + reconciled: false, + locationCode: "", + reportingLocationCode: "", + purchaseOrderNo: "", + referenceCode: "", + salespersonCode: "", + totalAmount: 107.31, + totalExempt: 0, + totalDiscount: 0, + totalTax: 10.2, + totalTaxable: 107.31, + totalTaxCalculated: 10.2, + adjustmentReason: AdjustmentReason.NotAdjusted, + locked: false, + version: 1, + exchangeRateEffectiveDate: new Date(), + exchangeRate: 1, + modifiedDate: new Date(), + modifiedUserId: 6479978, + taxDate: new Date(), + lines: [ + { + id: 0, + transactionId: 0, + lineNumber: "1", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "", + lineAmount: 36.53, + quantity: 2, + ref1: "", + ref2: "", + reportingDate: new Date(), + tax: 3.47, + taxableAmount: 36.53, + taxCalculated: 3.47, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date(), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 2.19, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 2.19, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 2.19, + reportingTaxCalculated: 2.19, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.09, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.09, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.09, + reportingTaxCalculated: 0.09, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 0.82, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.82, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.82, + reportingTaxCalculated: 0.82, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.37, + taxableAmount: 36.53, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.37, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 36.53, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.37, + reportingTaxCalculated: 0.37, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + { + id: 0, + transactionId: 0, + lineNumber: "2", + customerUsageType: "", + entityUseCode: "", + discountAmount: 0, + exemptAmount: 0, + exemptCertId: 0, + exemptNo: "", + isItemTaxable: true, + itemCode: "Shipping", + lineAmount: 70.78, + quantity: 1, + ref1: "", + ref2: "", + reportingDate: new Date(), + tax: 6.73, + taxableAmount: 70.78, + taxCalculated: 6.73, + taxCode: "P0000000", + taxCodeId: 8087, + taxDate: new Date(), + taxIncluded: true, + details: [ + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "06", + jurisName: "CALIFORNIA", + stateAssignedNo: "", + jurisType: JurisTypeId.STA, + jurisdictionType: JurisdictionType.State, + nonTaxableAmount: 0, + rate: 0.06, + tax: 4.25, + taxableAmount: 70.78, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA STATE TAX", + taxAuthorityTypeId: 45, + taxCalculated: 4.25, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 70.78, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 4.25, + reportingTaxCalculated: 4.25, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "037", + jurisName: "LOS ANGELES", + stateAssignedNo: "", + jurisType: JurisTypeId.CTY, + jurisdictionType: JurisdictionType.County, + nonTaxableAmount: 0, + rate: 0.0025, + tax: 0.18, + taxableAmount: 70.78, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA COUNTY TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.18, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 70.78, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.18, + reportingTaxCalculated: 0.18, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + stateAssignedNo: "594", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.0225, + tax: 1.59, + taxableAmount: 70.78, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 1.59, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 70.78, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 1.59, + reportingTaxCalculated: 1.59, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + { + id: 0, + transactionLineId: 0, + transactionId: 0, + country: "US", + region: "CA", + exemptAmount: 0, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + stateAssignedNo: "19", + jurisType: JurisTypeId.STJ, + jurisdictionType: JurisdictionType.Special, + nonTaxableAmount: 0, + rate: 0.01, + tax: 0.71, + taxableAmount: 70.78, + taxType: "Use", + taxSubTypeId: "U", + taxName: "CA SPECIAL TAX", + taxAuthorityTypeId: 45, + taxCalculated: 0.71, + rateType: RateType.General, + rateTypeCode: "G", + unitOfBasis: "PerCurrencyUnit", + isNonPassThru: false, + isFee: false, + reportingTaxableUnits: 70.78, + reportingNonTaxableUnits: 0, + reportingExemptUnits: 0, + reportingTax: 0.71, + reportingTaxCalculated: 0.71, + liabilityType: LiabilityType.Seller, + chargedTo: ChargedTo.Buyer, + }, + ], + nonPassthroughDetails: [], + hsCode: "", + costInsuranceFreight: 0, + vatCode: "", + vatNumberTypeId: 0, + }, + ], + addresses: [ + { + id: 0, + transactionId: 0, + boundaryLevel: BoundaryLevel.Zip5, + line1: "123 Palm Grove Ln", + line2: "", + line3: "", + city: "LOS ANGELES", + region: "CA", + postalCode: "90002", + country: "US", + taxRegionId: 4017056, + latitude: "33.948712", + longitude: "-118.245951", + }, + { + id: 0, + transactionId: 0, + boundaryLevel: BoundaryLevel.Zip5, + line1: "8559 Lake Avenue", + line2: "", + line3: "", + city: "New York", + region: "NY", + postalCode: "10001", + country: "US", + taxRegionId: 2088629, + latitude: "40.748481", + longitude: "-73.993125", + }, + ], + summary: [ + { + country: "US", + region: "CA", + jurisType: JurisdictionType.State, + jurisCode: "06", + jurisName: "CALIFORNIA", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Use", + taxSubType: "U", + taxName: "CA STATE TAX", + rateType: RateType.General, + taxable: 107.31, + rate: 0.06, + tax: 6.44, + taxCalculated: 6.44, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.County, + jurisCode: "037", + jurisName: "LOS ANGELES", + taxAuthorityType: 45, + stateAssignedNo: "", + taxType: "Use", + taxSubType: "U", + taxName: "CA COUNTY TAX", + rateType: RateType.General, + taxable: 107.31, + rate: 0.0025, + tax: 0.27, + taxCalculated: 0.27, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMTC0", + jurisName: "LOS ANGELES CO LOCAL TAX SL", + taxAuthorityType: 45, + stateAssignedNo: "19", + taxType: "Use", + taxSubType: "U", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 107.31, + rate: 0.01, + tax: 1.08, + taxCalculated: 1.08, + nonTaxable: 0, + exemption: 0, + }, + { + country: "US", + region: "CA", + jurisType: JurisdictionType.Special, + jurisCode: "EMAR0", + jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP", + taxAuthorityType: 45, + stateAssignedNo: "594", + taxType: "Use", + taxSubType: "U", + taxName: "CA SPECIAL TAX", + rateType: RateType.General, + taxable: 107.31, + rate: 0.0225, + tax: 2.41, + taxCalculated: 2.41, + nonTaxable: 0, + exemption: 0, + }, + ], +}; + +describe("AvataxOrderCreatedResponseTransformer", () => { + it("returns orded id in response", () => { + const transformer = new AvataxOrderCreatedResponseTransformer(); + const result = transformer.transform(MOCKED_TRANSACTION); + + expect(result).toEqual({ + id: "8fc875ce-a929-4556-9f30-0165b1597d9f", + }); + }); + it("throws an error when no transaction id is present", () => { + const transformer = new AvataxOrderCreatedResponseTransformer(); + + expect(() => transformer.transform({} as any)).toThrowError(); + }); +}); diff --git a/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transformer.ts b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transformer.ts new file mode 100644 index 0000000..890a9cb --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-created/avatax-order-created-response-transformer.ts @@ -0,0 +1,16 @@ +import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; + +export class AvataxOrderCreatedResponseTransformer { + transform(response: TransactionModel): CreateOrderResponse { + 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." + ) + ), + }; + } +} diff --git a/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-adapter.ts b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-adapter.ts new file mode 100644 index 0000000..4841ebf --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-adapter.ts @@ -0,0 +1,41 @@ +import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql"; +import { Logger, createLogger } from "../../../lib/logger"; +import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; +import { AvataxClient, CommitTransactionArgs } from "../avatax-client"; +import { AvataxConfig } from "../avatax-config"; +import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer"; +import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer"; + +export type Payload = { + order: OrderFulfilledSubscriptionFragment; + config: AvataxConfig; +}; +export type Target = CommitTransactionArgs; +export type Response = { ok: true }; + +export class AvataxOrderFulfilledAdapter implements WebhookAdapter { + private logger: Logger; + + constructor(private readonly config: AvataxConfig) { + this.logger = createLogger({ service: "AvataxOrderFulfilledAdapter" }); + } + + async send(payload: Pick): Promise { + this.logger.debug({ payload }, "send called with:"); + + const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer(); + const target = payloadTransformer.transform({ ...payload, config: this.config }); + + const client = new AvataxClient(this.config); + const response = await client.commitTransaction(target); + + this.logger.debug({ response }, "Avatax commitTransaction response:"); + + const responseTransformer = new AvataxOrderFulfilledResponseTransformer(); + const transformedResponse = responseTransformer.transform(response); + + this.logger.debug({ transformedResponse }, "Transformed Avatax commitTransaction response to:"); + + return transformedResponse; + } +} diff --git a/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer.test.ts b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer.test.ts new file mode 100644 index 0000000..9d8a675 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer.test.ts @@ -0,0 +1,144 @@ +import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { describe, expect, it } from "vitest"; +import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql"; +import { AvataxConfig } from "../avatax-config"; +import { Payload } from "./avatax-order-fulfilled-adapter"; +import { + AvataxOrderFulfilledPayloadTransformer, + PROVIDER_ORDER_ID_KEY, + getTransactionCodeFromMetadata, +} from "./avatax-order-fulfilled-payload-transformer"; + +// todo: add AvataxOrderFulfilledMockGenerator + +const MOCK_AVATAX_CONFIG: AvataxConfig = { + companyCode: "DEFAULT", + isAutocommit: false, + isSandbox: true, + name: "Avatax-1", + password: "password", + username: "username", + shippingTaxCode: "FR000000", +}; + +const MOCKED_METADATA: OrderFulfilledSubscriptionFragment["privateMetadata"] = [ + { + key: PROVIDER_ORDER_ID_KEY, + value: "transaction-code", + }, +]; + +type OrderFulfilled = OrderFulfilledSubscriptionFragment; + +const ORDER_FULFILLED_MOCK: OrderFulfilled = { + id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5", + created: "2023-04-11T11:03:09.304109+00:00", + privateMetadata: MOCKED_METADATA, + channel: { + id: "Q2hhbm5lbDoy", + slug: "channel-pln", + }, + shippingAddress: { + streetAddress1: "123 Palm Grove Ln", + streetAddress2: "", + city: "LOS ANGELES", + countryArea: "CA", + postalCode: "90002", + country: { + code: "US", + }, + }, + billingAddress: { + streetAddress1: "123 Palm Grove Ln", + streetAddress2: "", + city: "LOS ANGELES", + countryArea: "CA", + postalCode: "90002", + country: { + code: "US", + }, + }, + total: { + net: { + amount: 183.33, + }, + tax: { + amount: 12.83, + }, + }, + shippingPrice: { + net: { + amount: 48.33, + }, + }, + lines: [ + { + productSku: "328223581", + productName: "Monospace Tee", + quantity: 1, + unitPrice: { + net: { + amount: 90, + }, + }, + totalPrice: { + net: { + amount: 90, + }, + tax: { + amount: 8.55, + }, + }, + }, + { + productSku: "328223580", + productName: "Polyspace Tee", + quantity: 1, + unitPrice: { + net: { + amount: 45, + }, + }, + totalPrice: { + net: { + amount: 45, + }, + tax: { + amount: 4.28, + }, + }, + }, + ], +}; + +describe("getTransactionCodeFromMetadata", () => { + it("returns transaction code", () => { + expect(getTransactionCodeFromMetadata(MOCKED_METADATA)).toBe("transaction-code"); + }); + + it("throws error when transaction code not found", () => { + expect(() => getTransactionCodeFromMetadata([])).toThrowError(); + }); +}); + +const transformer = new AvataxOrderFulfilledPayloadTransformer(); + +const MOCKED_ORDER_FULFILLED_PAYLOAD: Payload = { + order: ORDER_FULFILLED_MOCK, + config: MOCK_AVATAX_CONFIG, +}; + +describe("AvataxOrderFulfilledPayloadTransformer", () => { + it("returns transformed payload", () => { + const mappedPayload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD); + + expect(mappedPayload).toEqual({ + transactionCode: "transaction-code", + companyCode: "DEFAULT", + documentType: DocumentType.SalesInvoice, + model: { + commit: true, + }, + }); + }); +}); diff --git a/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer.ts b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer.ts new file mode 100644 index 0000000..1dc2d59 --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer.ts @@ -0,0 +1,34 @@ +import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql"; +import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { Payload, Target } from "./avatax-order-fulfilled-adapter"; + +// * This is the key that we use to store the provider order id in the Saleor order metadata. + +export const PROVIDER_ORDER_ID_KEY = "externalId"; + +export function getTransactionCodeFromMetadata( + metadata: OrderFulfilledSubscriptionFragment["privateMetadata"] +) { + const transactionCode = metadata.find((item) => item.key === PROVIDER_ORDER_ID_KEY); + + if (!transactionCode) { + throw new Error("Transaction code not found"); + } + + return transactionCode.value; +} + +export class AvataxOrderFulfilledPayloadTransformer { + transform({ order, config }: Payload): Target { + const transactionCode = getTransactionCodeFromMetadata(order.privateMetadata); + + return { + transactionCode, + companyCode: config.companyCode ?? "", + documentType: DocumentType.SalesInvoice, + model: { + commit: true, + }, + }; + } +} diff --git a/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-response-transformer.ts b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-response-transformer.ts new file mode 100644 index 0000000..801422b --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-fulfilled/avatax-order-fulfilled-response-transformer.ts @@ -0,0 +1,8 @@ +import { TransactionModel } from "avatax/lib/models/TransactionModel"; +import { Response } from "./avatax-order-fulfilled-adapter"; + +export class AvataxOrderFulfilledResponseTransformer { + transform(response: TransactionModel): Response { + return { ok: true }; + } +} diff --git a/apps/taxes/src/modules/taxes/active-tax-provider.test.ts b/apps/taxes/src/modules/taxes/active-tax-provider.test.ts index f31ccdc..ba2d5de 100644 --- a/apps/taxes/src/modules/taxes/active-tax-provider.test.ts +++ b/apps/taxes/src/modules/taxes/active-tax-provider.test.ts @@ -79,61 +79,49 @@ const mockedValidEncryptedChannels = encrypt(JSON.stringify(mockedValidChannels) vi.stubEnv("SECRET_KEY", mockedSecretKey); describe("getActiveTaxProvider", () => { - it("should return ok: false when channel slug is missing", () => { - const result = getActiveTaxProvider("", mockedInvalidMetadata); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toBe("channel_slug_missing"); - } + it("throws error when channel slug is missing", () => { + expect(() => getActiveTaxProvider("", mockedInvalidMetadata)).toThrow( + "Channel slug is missing" + ); }); - it("should return ok: false when there are no metadata items", () => { - const result = getActiveTaxProvider("default-channel", []); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toBe("app_encrypted_metadata_missing"); - } + it("throws error when there are no metadata items", () => { + expect(() => getActiveTaxProvider("default-channel", [])).toThrow( + "App encryptedMetadata is missing" + ); }); - it("should return ok: false when no providerInstanceId was found", () => { - const result = getActiveTaxProvider("default-channel", [ - { - key: "providers", - value: mockedEncryptedProviders, - }, - { - key: "channels", - value: mockedInvalidEncryptedChannels, - }, - ]); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toBe("no_match_for_channel_provider_instance_id"); - } + it("throws error when no providerInstanceId was found", () => { + expect(() => + getActiveTaxProvider("default-channel", [ + { + key: "providers", + value: mockedEncryptedProviders, + }, + { + key: "channels", + value: mockedInvalidEncryptedChannels, + }, + ]) + ).toThrow("Channel (default-channel) providerInstanceId does not match any providers"); }); - it("should return ok: false when no channel was found for channelSlug", () => { - const result = getActiveTaxProvider("invalid-channel", [ - { - key: "providers", - value: mockedEncryptedProviders, - }, - { - key: "channels", - value: mockedValidEncryptedChannels, - }, - ]); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toBe("channel_config_not_found"); - } + it("throws error when no channel was found for channelSlug", () => { + expect(() => + getActiveTaxProvider("invalid-channel", [ + { + key: "providers", + value: mockedEncryptedProviders, + }, + { + key: "channels", + value: mockedValidEncryptedChannels, + }, + ]) + ).toThrow("Channel config not found for channel invalid-channel"); }); - it("should return ok: true when data is correct", () => { + it("returns provider when data is correct", () => { const result = getActiveTaxProvider("default-channel", [ { key: "providers", @@ -145,6 +133,6 @@ describe("getActiveTaxProvider", () => { }, ]); - expect(result.ok).toBe(true); + expect(result).toBeDefined(); }); }); diff --git a/apps/taxes/src/modules/taxes/active-tax-provider.ts b/apps/taxes/src/modules/taxes/active-tax-provider.ts index 316ee92..14df6ef 100644 --- a/apps/taxes/src/modules/taxes/active-tax-provider.ts +++ b/apps/taxes/src/modules/taxes/active-tax-provider.ts @@ -4,61 +4,14 @@ import { OrderFulfilledSubscriptionFragment, TaxBaseFragment, } from "../../../generated/graphql"; -import { createLogger, Logger } from "../../lib/logger"; -import { ChannelConfig } from "../channels-configuration/channels-config"; -import { ProviderConfig } from "../providers-configuration/providers-config"; -import { AvataxWebhookService } from "../avatax/avatax-webhook.service"; -import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service"; -import { ProviderWebhookService } from "./tax-provider-webhook"; -import { TaxProviderError } from "./tax-provider-error"; +import { Logger, createLogger } from "../../lib/logger"; import { getAppConfig } from "../app/get-app-config"; - -type ActiveTaxProviderResult = { ok: true; data: ActiveTaxProvider } | { ok: false; error: string }; - -export function getActiveTaxProvider( - channelSlug: string | undefined, - encryptedMetadata: MetadataItem[] -): ActiveTaxProviderResult { - const logger = createLogger({ service: "getActiveTaxProvider" }); - - if (!channelSlug) { - logger.error("Channel slug is missing"); - return { error: "channel_slug_missing", ok: false }; - } - - if (!encryptedMetadata.length) { - logger.error("App encryptedMetadata is missing"); - return { error: "app_encrypted_metadata_missing", ok: false }; - } - - const { providers, channels } = getAppConfig(encryptedMetadata); - - const channelConfig = channels[channelSlug]; - - if (!channelConfig) { - // * will happen when `order-created` webhook is triggered by creating an order in a channel that doesn't use the tax app - logger.info(`Channel config not found for channel ${channelSlug}`); - return { error: `channel_config_not_found`, ok: false }; - } - - const providerInstance = providers.find( - (instance) => instance.id === channelConfig.providerInstanceId - ); - - if (!providerInstance) { - logger.error(`Channel (${channelSlug}) providerInstanceId does not match any providers`); - return { - error: `no_match_for_channel_provider_instance_id`, - ok: false, - }; - } - - // todo: refactor so it doesnt create activeTaxProvider - const taxProvider = new ActiveTaxProvider(providerInstance, channelConfig); - - return { data: taxProvider, ok: true }; -} +import { AvataxWebhookService } from "../avatax/avatax-webhook.service"; +import { ChannelConfig } from "../channels-configuration/channels-config"; +import { ProviderConfig } from "../providers-configuration/providers-config"; +import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service"; +import { ProviderWebhookService } from "./tax-provider-webhook"; // todo: refactor to a factory export class ActiveTaxProvider implements ProviderWebhookService { @@ -86,28 +39,60 @@ export class ActiveTaxProvider implements ProviderWebhookService { break; default: { - throw new TaxProviderError(`Tax provider ${taxProviderName} doesn't match`, { - cause: "TaxProviderNotFound", - }); + throw new Error(`Tax provider ${taxProviderName} doesn't match`); } } } async calculateTaxes(payload: TaxBaseFragment) { - this.logger.debug({ payload }, ".calculate called"); + this.logger.trace({ payload }, ".calculate called"); return this.client.calculateTaxes(payload, this.channel); } async createOrder(order: OrderCreatedSubscriptionFragment) { - this.logger.debug(".createOrder called"); + this.logger.trace(".createOrder called"); return this.client.createOrder(order, this.channel); } async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) { - this.logger.debug(".fulfillOrder called"); + this.logger.trace(".fulfillOrder called"); return this.client.fulfillOrder(payload, this.channel); } } + +export function getActiveTaxProvider( + channelSlug: string | undefined, + encryptedMetadata: MetadataItem[] +): ActiveTaxProvider { + if (!channelSlug) { + throw new Error("Channel slug is missing"); + } + + if (!encryptedMetadata.length) { + throw new Error("App encryptedMetadata is missing"); + } + + const { providers, channels } = getAppConfig(encryptedMetadata); + + const channelConfig = channels[channelSlug]; + + if (!channelConfig) { + // * will happen when `order-created` webhook is triggered by creating an order in a channel that doesn't use the tax app + throw new Error(`Channel config not found for channel ${channelSlug}`); + } + + const providerInstance = providers.find( + (instance) => instance.id === channelConfig.providerInstanceId + ); + + if (!providerInstance) { + throw new Error(`Channel (${channelSlug}) providerInstanceId does not match any providers`); + } + + const taxProvider = new ActiveTaxProvider(providerInstance, channelConfig); + + return taxProvider; +} diff --git a/apps/taxes/src/modules/taxes/discount-utils.test.ts b/apps/taxes/src/modules/taxes/discount-utils.test.ts new file mode 100644 index 0000000..2486a04 --- /dev/null +++ b/apps/taxes/src/modules/taxes/discount-utils.test.ts @@ -0,0 +1,59 @@ +import { expect, describe, it } from "vitest"; +import { discountUtils } from "./discount-utils"; + +describe("discountUtils", () => { + describe("distributeDiscount", () => { + it("returns a numbers array thats sum is equal original sum - the discount", () => { + const discount = 10; + const nums = [42, 55, 67, 49]; + + const result = discountUtils.distributeDiscount(discount, nums); + const resultSum = result.reduce((acc, curr) => acc + curr, 0); + + expect(resultSum).toEqual(discount); + }); + it("returns a numbers array where all items are >= 0", () => { + const discount = 10; + const nums = [1, 2, 3, 5]; + + const result = discountUtils.distributeDiscount(discount, nums); + + expect(result.every((num) => num >= 0)).toBe(true); + }); + it("throws an error when discount is greater than the sum of the numbers array", () => { + const discount = 100; + const nums = [1, 2, 3, 5]; + + expect(() => discountUtils.distributeDiscount(discount, nums)).toThrowError(); + }); + it("returns the same numbers when no discount", () => { + const discount = 0; + const nums = [1, 2, 3, 5]; + + const result = discountUtils.distributeDiscount(discount, nums); + + expect(result).toEqual([0, 0, 0, 0]); + }); + it("returns throw error when discount = 0 and numbers = 0", () => { + const discount = 0; + const nums = [0, 0, 0, 0]; + + expect(() => discountUtils.distributeDiscount(discount, nums)).toThrowError(); + }); + }); + + describe("sumDiscounts", () => { + it("sums up all discounts", () => { + const discountsArray = [1, 2, 3, 4]; + const discounts = discountUtils.sumDiscounts(discountsArray); + + expect(discounts).toEqual(10); + }); + + it("returns 0 if there are no discounts", () => { + const discounts = discountUtils.sumDiscounts([]); + + expect(discounts).toEqual(0); + }); + }); +}); diff --git a/apps/taxes/src/modules/taxes/discount-utils.ts b/apps/taxes/src/modules/taxes/discount-utils.ts new file mode 100644 index 0000000..f4219b8 --- /dev/null +++ b/apps/taxes/src/modules/taxes/discount-utils.ts @@ -0,0 +1,42 @@ +import { numbers } from "./numbers"; + +// ? shouldn't it be used in all providers? + +/* + * Saleor provides discounts as an array of objects with an amount. This function takes in the sum of those discounts and the prices of the line items and returns an array of numbers that represent the discount for each item. You can then use this array to return the individual discounts or to calculate the discounted prices. + */ + +/* + * // todo: look into how refunds affect the prices and discounts: + * https://github.com/saleor/apps/pull/495#discussion_r1200321165 + */ +function distributeDiscount(discountSum: number, prices: number[]) { + const totalSum = prices.reduce((sum, number) => sum + number, 0); + + if (discountSum > totalSum) { + throw new Error("Discount cannot be greater than total sum of line prices."); + } + + if (totalSum === 0) { + throw new Error("Cannot distribute discount when total sum is 0."); + } + + const discountRatio = discountSum / totalSum; + + const distributedDiscounts = prices.map((number) => { + const discountAmount = number * discountRatio; + + return numbers.roundFloatToTwoDecimals(Number(discountAmount)); + }); + + return distributedDiscounts; +} + +function sumDiscounts(discounts: number[]): number { + return discounts.reduce((total, current) => total + Number(current), 0); +} + +export const discountUtils = { + distributeDiscount, + sumDiscounts, +}; diff --git a/apps/taxes/src/modules/taxes/tax-provider-error.ts b/apps/taxes/src/modules/taxes/tax-provider-error.ts deleted file mode 100644 index bd369d3..0000000 --- a/apps/taxes/src/modules/taxes/tax-provider-error.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TaxProviderValidationError = "TaxProviderNotFound"; - -type TaxProviderErrorName = TaxProviderValidationError; -export class TaxProviderError extends Error { - constructor(message: string, options: { cause: TaxProviderErrorName }) { - super(message, options); - } -} diff --git a/apps/taxes/src/modules/taxes/tax-provider-utils-test.ts b/apps/taxes/src/modules/taxes/tax-provider-utils.test.ts similarity index 75% rename from apps/taxes/src/modules/taxes/tax-provider-utils-test.ts rename to apps/taxes/src/modules/taxes/tax-provider-utils.test.ts index fca689d..2fa6026 100644 --- a/apps/taxes/src/modules/taxes/tax-provider-utils-test.ts +++ b/apps/taxes/src/modules/taxes/tax-provider-utils.test.ts @@ -5,15 +5,15 @@ import { taxProviderUtils } from "./tax-provider-utils"; describe("taxProviderUtils", () => { describe("resolveOptionalOrThrow", () => { - it("should throw a default error if value is undefined", () => { + it("throws a default error if value is undefined", () => { expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined)).toThrowError(); }); - it("should throw a custom error if value is undefined", () => { + it("throws a custom error if value is undefined", () => { expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined, new Error("test")) ).toThrowError("test"); }), - it("should return value if value is not undefined", () => { + it("returns value if value is not undefined", () => { expect(taxProviderUtils.resolveOptionalOrThrow("test")).toBe("test"); }); }); diff --git a/apps/taxes/src/modules/taxes/tax-webhook-adapter.ts b/apps/taxes/src/modules/taxes/tax-webhook-adapter.ts new file mode 100644 index 0000000..d58d271 --- /dev/null +++ b/apps/taxes/src/modules/taxes/tax-webhook-adapter.ts @@ -0,0 +1,3 @@ +export interface WebhookAdapter, TResponse extends any> { + send(payload: TPayload): Promise; +} diff --git a/apps/taxes/src/modules/taxjar/maps/address-factory.test.ts b/apps/taxes/src/modules/taxjar/address-factory.test.ts similarity index 75% rename from apps/taxes/src/modules/taxjar/maps/address-factory.test.ts rename to apps/taxes/src/modules/taxjar/address-factory.test.ts index 4f02500..335eb64 100644 --- a/apps/taxes/src/modules/taxjar/maps/address-factory.test.ts +++ b/apps/taxes/src/modules/taxjar/address-factory.test.ts @@ -13,11 +13,11 @@ describe("taxJarAddressFactory", () => { }); expect(result).toEqual({ - street: "123 Palm Grove Ln", - city: "LOS ANGELES", - state: "CA", - zip: "90002", - country: "US", + from_street: "123 Palm Grove Ln", + from_city: "LOS ANGELES", + from_state: "CA", + from_zip: "90002", + from_country: "US", }); }); }); @@ -36,11 +36,11 @@ describe("taxJarAddressFactory", () => { }); expect(result).toEqual({ - street: "123 Palm Grove Ln", - city: "LOS ANGELES", - state: "CA", - zip: "90002", - country: "US", + to_street: "123 Palm Grove Ln", + to_city: "LOS ANGELES", + to_state: "CA", + to_zip: "90002", + to_country: "US", }); }); @@ -57,11 +57,11 @@ describe("taxJarAddressFactory", () => { }); expect(result).toEqual({ - street: "123 Palm Grove Ln", - city: "LOS ANGELES", - state: "CA", - zip: "90002", - country: "US", + to_street: "123 Palm Grove Ln", + to_city: "LOS ANGELES", + to_state: "CA", + to_zip: "90002", + to_country: "US", }); }); }); diff --git a/apps/taxes/src/modules/taxjar/address-factory.ts b/apps/taxes/src/modules/taxjar/address-factory.ts new file mode 100644 index 0000000..e6e41de --- /dev/null +++ b/apps/taxes/src/modules/taxjar/address-factory.ts @@ -0,0 +1,36 @@ +import { ChannelAddress } from "../channels-configuration/channels-config"; +import { AddressFragment as SaleorAddress } from "../../../generated/graphql"; +import { AddressParams as TaxJarAddress, TaxParams } from "taxjar/dist/types/paramTypes"; + +function joinAddresses(address1: string, address2: string): string { + return `${address1}${address2.length > 0 ? " " + address2 : ""}`; +} + +function mapSaleorAddressToTaxJarAddress( + address: SaleorAddress +): Pick { + return { + to_street: joinAddresses(address.streetAddress1, address.streetAddress2), + to_city: address.city, + to_zip: address.postalCode, + to_state: address.countryArea, + to_country: address.country.code, + }; +} + +function mapChannelAddressToTaxJarAddress( + address: ChannelAddress +): Pick { + return { + from_city: address.city, + from_country: address.country, + from_state: address.state, + from_street: address.street, + from_zip: address.zip, + }; +} + +export const taxJarAddressFactory = { + fromSaleorAddress: mapSaleorAddressToTaxJarAddress, + fromChannelAddress: mapChannelAddressToTaxJarAddress, +}; diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.test.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.test.ts new file mode 100644 index 0000000..525de05 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.test.ts @@ -0,0 +1,3 @@ +import { describe } from "vitest"; + +describe.todo("TaxJarCalculateTaxesAdapter", () => {}); diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts new file mode 100644 index 0000000..03aaeb5 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts @@ -0,0 +1,44 @@ +import { TaxBaseFragment } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook"; +import { FetchTaxForOrderArgs, TaxJarClient } from "../taxjar-client"; +import { TaxJarConfig } from "../taxjar-config"; +import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; +import { TaxJarCalculateTaxesPayloadTransformer } from "./taxjar-calculate-taxes-payload-transformer"; +import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer"; +import { Logger, createLogger } from "../../../lib/logger"; + +export type Payload = { + taxBase: TaxBaseFragment; + channelConfig: ChannelConfig; +}; + +export type Target = FetchTaxForOrderArgs; +export type Response = CalculateTaxesResponse; + +export class TaxJarCalculateTaxesAdapter implements WebhookAdapter { + private logger: Logger; + constructor(private readonly config: TaxJarConfig) { + this.logger = createLogger({ service: "TaxJarCalculateTaxesAdapter" }); + } + + async send(payload: Payload): Promise { + this.logger.debug({ payload }, "send called with:"); + const payloadTransformer = new TaxJarCalculateTaxesPayloadTransformer(); + const target = payloadTransformer.transform(payload); + + this.logger.debug({ transformedPayload: target }, "Will call fetchTaxForOrder with:"); + + const client = new TaxJarClient(this.config); + const response = await client.fetchTaxForOrder(target); + + this.logger.debug({ response }, "TaxJar fetchTaxForOrder response:"); + + const responseTransformer = new TaxJarCalculateTaxesResponseTransformer(); + const transformedResponse = responseTransformer.transform(payload, response); + + this.logger.debug({ transformedResponse }, "Transformed TaxJar fetchTaxForOrder response to:"); + + return transformedResponse; + } +} diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts new file mode 100644 index 0000000..51a959d --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts @@ -0,0 +1,505 @@ +import { TaxForOrderRes } from "taxjar/dist/types/returnTypes"; +import { TaxBaseFragment } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; + +type TaxBase = TaxBaseFragment; + +const taxIncludedTaxBase: TaxBase = { + pricesEnteredWithTax: true, + currency: "USD", + channel: { + slug: "default-channel", + }, + discounts: [], + address: { + streetAddress1: "668 Route Six", + streetAddress2: "", + city: "MAHOPAC", + countryArea: "NY", + postalCode: "10541", + country: { + code: "US", + }, + }, + shippingPrice: { + amount: 59.17, + }, + lines: [ + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOmM5MTUxMDljLTBkMzEtNDg2Yy05OGFmLTQ5NDM0MWY4NTNjYw==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQ4", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 3, + unitPrice: { + amount: 20, + }, + totalPrice: { + amount: 60, + }, + }, + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjUxZDc2ZDY1LTFhYTgtNGEzMi1hNWJhLTJkZDMzNjVhZDhlZQ==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQ5", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 1, + unitPrice: { + amount: 20, + }, + totalPrice: { + amount: 20, + }, + }, + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjlhMGJjZDhmLWFiMGQtNDJhOC04NTBhLTEyYjQ2YjJiNGIyZg==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQw", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 2, + unitPrice: { + amount: 50, + }, + totalPrice: { + amount: 100, + }, + }, + ], + sourceObject: { + user: { + id: "VXNlcjoyMDg0NTEwNDEw", + }, + }, +}; + +const taxExcludedTaxBase: TaxBase = { + pricesEnteredWithTax: false, + currency: "USD", + channel: { + slug: "default-channel", + }, + discounts: [], + address: { + streetAddress1: "668 Route Six", + streetAddress2: "", + city: "MAHOPAC", + countryArea: "NY", + postalCode: "10541", + country: { + code: "US", + }, + }, + shippingPrice: { + amount: 59.17, + }, + lines: [ + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOmM5MTUxMDljLTBkMzEtNDg2Yy05OGFmLTQ5NDM0MWY4NTNjYw==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQ4", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 3, + unitPrice: { + amount: 20, + }, + totalPrice: { + amount: 60, + }, + }, + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjUxZDc2ZDY1LTFhYTgtNGEzMi1hNWJhLTJkZDMzNjVhZDhlZQ==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQ5", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 1, + unitPrice: { + amount: 20, + }, + totalPrice: { + amount: 20, + }, + }, + { + sourceLine: { + __typename: "OrderLine", + id: "T3JkZXJMaW5lOjlhMGJjZDhmLWFiMGQtNDJhOC04NTBhLTEyYjQ2YjJiNGIyZg==", + variant: { + id: "UHJvZHVjdFZhcmlhbnQ6MzQw", + product: { + metafield: null, + productType: { + metafield: null, + }, + }, + }, + }, + quantity: 2, + unitPrice: { + amount: 50, + }, + totalPrice: { + amount: 100, + }, + }, + ], + sourceObject: { + user: { + id: "VXNlcjoyMDg0NTEwNDEw", + }, + }, +}; + +const withNexusChannelConfig: ChannelConfig = { + providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081", + enabled: false, + address: { + country: "US", + zip: "10118", + state: "NY", + city: "New York", + street: "350 5th Avenue", + }, +}; + +const noNexusChannelConfig: ChannelConfig = { + providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50", + enabled: false, + address: { + country: "US", + zip: "10118", + state: "NY", + city: "New York", + street: "350 5th Avenue", + }, +}; + +type TaxForOrder = TaxForOrderRes; + +const noNexusTaxForOrderMock: TaxForOrder = { + tax: { + amount_to_collect: 0, + freight_taxable: false, + has_nexus: false, + order_total_amount: 0, + rate: 0, + shipping: 0, + tax_source: "", + taxable_amount: 0, + exemption_type: "", + jurisdictions: { + country: "", + }, + }, +}; + +const withNexusTaxExcludedTaxForOrderMock: TaxForOrder = { + tax: { + exemption_type: "", + amount_to_collect: 20.03, + breakdown: { + city_tax_collectable: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_tax_collectable: 10.46, + county_tax_rate: 0.04375, + county_taxable_amount: 239.17, + line_items: [ + { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 0.88, + county_tax_rate: 0.04375, + county_taxable_amount: 20, + id: taxExcludedTaxBase.lines[0].sourceLine.id, + special_district_amount: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_amount: 0.8, + state_sales_tax_rate: 0.04, + state_taxable_amount: 20, + tax_collectable: 1.68, + taxable_amount: 20, + }, + { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 4.38, + county_tax_rate: 0.04375, + county_taxable_amount: 100, + id: taxExcludedTaxBase.lines[1].sourceLine.id, + special_district_amount: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_amount: 4, + state_sales_tax_rate: 0.04, + state_taxable_amount: 100, + tax_collectable: 8.38, + taxable_amount: 100, + }, + { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 2.63, + county_tax_rate: 0.04375, + county_taxable_amount: 60, + id: taxExcludedTaxBase.lines[2].sourceLine.id, + special_district_amount: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_amount: 2.4, + state_sales_tax_rate: 0.04, + state_taxable_amount: 60, + tax_collectable: 5.03, + taxable_amount: 60, + }, + ], + shipping: { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 2.59, + county_tax_rate: 0.04375, + county_taxable_amount: 59.17, + special_district_amount: 0, + special_tax_rate: 0, + special_taxable_amount: 0, + state_amount: 2.37, + state_sales_tax_rate: 0.04, + state_taxable_amount: 59.17, + tax_collectable: 4.96, + taxable_amount: 59.17, + }, + special_district_tax_collectable: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_tax_collectable: 9.57, + state_tax_rate: 0.04, + state_taxable_amount: 239.17, + tax_collectable: 20.03, + taxable_amount: 239.17, + }, + freight_taxable: true, + has_nexus: true, + jurisdictions: { + city: "MAHOPAC", + country: "US", + county: "PUTNAM", + state: "NY", + }, + order_total_amount: 239.17, + rate: 0.08375, + shipping: 59.17, + tax_source: "destination", + taxable_amount: 239.17, + }, +}; + +const withNexusTaxIncludedTaxForOrderMock: TaxForOrder = { + tax: { + exemption_type: "", + amount_to_collect: 20.03, + breakdown: { + city_tax_collectable: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_tax_collectable: 10.46, + county_tax_rate: 0.04375, + county_taxable_amount: 239.17, + line_items: [ + { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 0.88, + county_tax_rate: 0.04375, + county_taxable_amount: 20, + id: taxIncludedTaxBase.lines[0].sourceLine.id, + special_district_amount: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_amount: 0.8, + state_sales_tax_rate: 0.04, + state_taxable_amount: 20, + tax_collectable: 1.68, + taxable_amount: 20, + }, + { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 4.38, + county_tax_rate: 0.04375, + county_taxable_amount: 100, + id: taxIncludedTaxBase.lines[1].sourceLine.id, + special_district_amount: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_amount: 4, + state_sales_tax_rate: 0.04, + state_taxable_amount: 100, + tax_collectable: 8.38, + taxable_amount: 100, + }, + { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 2.63, + county_tax_rate: 0.04375, + county_taxable_amount: 60, + id: taxIncludedTaxBase.lines[2].sourceLine.id, + special_district_amount: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_amount: 2.4, + state_sales_tax_rate: 0.04, + state_taxable_amount: 60, + tax_collectable: 5.03, + taxable_amount: 60, + }, + ], + shipping: { + city_amount: 0, + city_tax_rate: 0, + city_taxable_amount: 0, + combined_tax_rate: 0.08375, + county_amount: 2.59, + county_tax_rate: 0.04375, + county_taxable_amount: 59.17, + special_district_amount: 0, + special_tax_rate: 0, + special_taxable_amount: 0, + state_amount: 2.37, + state_sales_tax_rate: 0.04, + state_taxable_amount: 59.17, + tax_collectable: 4.96, + taxable_amount: 59.17, + }, + special_district_tax_collectable: 0, + special_district_taxable_amount: 0, + special_tax_rate: 0, + state_tax_collectable: 9.57, + state_tax_rate: 0.04, + state_taxable_amount: 239.17, + tax_collectable: 20.03, + taxable_amount: 239.17, + }, + freight_taxable: true, + has_nexus: true, + jurisdictions: { + city: "MAHOPAC", + country: "US", + county: "PUTNAM", + state: "NY", + }, + order_total_amount: 239.17, + rate: 0.08375, + shipping: 59.17, + tax_source: "destination", + taxable_amount: 239.17, + }, +}; + +// with/without tax +const testingScenariosMap = { + with_no_nexus_tax_included: { + taxBase: taxIncludedTaxBase, + channelConfig: noNexusChannelConfig, + response: noNexusTaxForOrderMock, + }, + with_no_nexus_tax_excluded: { + taxBase: taxExcludedTaxBase, + channelConfig: noNexusChannelConfig, + response: noNexusTaxForOrderMock, + }, + with_nexus_tax_included: { + taxBase: taxIncludedTaxBase, + channelConfig: withNexusChannelConfig, + response: withNexusTaxIncludedTaxForOrderMock, + }, + with_nexus_tax_excluded: { + taxBase: taxExcludedTaxBase, + channelConfig: withNexusChannelConfig, + response: withNexusTaxExcludedTaxForOrderMock, + }, +}; + +type TestingScenario = keyof typeof testingScenariosMap; + +export class TaxJarCalculateTaxesMockGenerator { + constructor(private scenario: TestingScenario) {} + generateTaxBase = (overrides: Partial = {}): TaxBase => + structuredClone({ + ...testingScenariosMap[this.scenario].taxBase, + ...overrides, + }); + + generateChannelConfig = (overrides: Partial = {}): ChannelConfig => + structuredClone({ + ...testingScenariosMap[this.scenario].channelConfig, + ...overrides, + }); + + generateResponse = (overrides: Partial = {}): TaxForOrder => + structuredClone({ + ...testingScenariosMap[this.scenario].response, + ...overrides, + }); +} diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-payload-transformer.test.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-payload-transformer.test.ts new file mode 100644 index 0000000..c1b4b2a --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-payload-transformer.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import { TaxJarCalculateTaxesMockGenerator } from "./taxjar-calculate-taxes-mock-generator"; +import { TaxJarCalculateTaxesPayloadTransformer } from "./taxjar-calculate-taxes-payload-transformer"; + +const transformer = new TaxJarCalculateTaxesPayloadTransformer(); + +describe("TaxJarCalculateTaxesPayloadTransformer", () => { + it("returns payload containing line_items without discounts", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const taxBase = mockGenerator.generateTaxBase(); + const channelConfig = mockGenerator.generateChannelConfig(); + const transformedPayload = transformer.transform({ + taxBase, + channelConfig, + }); + + expect(transformedPayload).toEqual({ + params: { + from_country: "US", + from_zip: "10118", + from_state: "NY", + from_city: "New York", + from_street: "350 5th Avenue", + to_country: "US", + to_zip: "10541", + to_state: "NY", + to_city: "MAHOPAC", + to_street: "668 Route Six", + shipping: 59.17, + line_items: [ + { + id: "T3JkZXJMaW5lOmM5MTUxMDljLTBkMzEtNDg2Yy05OGFmLTQ5NDM0MWY4NTNjYw==", + quantity: 3, + unit_price: 20, + discount: 0, + product_tax_code: "", + }, + { + id: "T3JkZXJMaW5lOjUxZDc2ZDY1LTFhYTgtNGEzMi1hNWJhLTJkZDMzNjVhZDhlZQ==", + quantity: 1, + unit_price: 20, + discount: 0, + product_tax_code: "", + }, + { + discount: 0, + id: "T3JkZXJMaW5lOjlhMGJjZDhmLWFiMGQtNDJhOC04NTBhLTEyYjQ2YjJiNGIyZg==", + product_tax_code: "", + quantity: 2, + unit_price: 50, + }, + ], + }, + }); + }); + it("returns payload containing line_items with discounts", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const taxBase = mockGenerator.generateTaxBase({ + discounts: [ + { + amount: { amount: 10 }, + }, + ], + }); + const channelConfig = mockGenerator.generateChannelConfig(); + const transformedPayload = transformer.transform({ + taxBase, + channelConfig, + }); + + const payloadLines = transformedPayload.params.line_items ?? []; + const discountSum = payloadLines.reduce((sum, line) => sum + (line.discount ?? 0), 0); + + expect(transformedPayload.params.line_items).toEqual([ + { + id: "T3JkZXJMaW5lOmM5MTUxMDljLTBkMzEtNDg2Yy05OGFmLTQ5NDM0MWY4NTNjYw==", + quantity: 3, + unit_price: 20, + discount: 3.33, + product_tax_code: "", + }, + { + id: "T3JkZXJMaW5lOjUxZDc2ZDY1LTFhYTgtNGEzMi1hNWJhLTJkZDMzNjVhZDhlZQ==", + quantity: 1, + unit_price: 20, + discount: 1.11, + product_tax_code: "", + }, + { + discount: 5.56, + id: "T3JkZXJMaW5lOjlhMGJjZDhmLWFiMGQtNDJhOC04NTBhLTEyYjQ2YjJiNGIyZg==", + product_tax_code: "", + quantity: 2, + unit_price: 50, + }, + ]); + + expect(discountSum).toEqual(10); + }); + it("throws error when no address", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const taxBase = mockGenerator.generateTaxBase({ address: null }); + const channelConfig = mockGenerator.generateChannelConfig(); + + expect(() => + transformer.transform({ + taxBase, + channelConfig, + }) + ).toThrow("Customer address is required to calculate taxes in TaxJar."); + }); +}); diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-payload-transformer.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-payload-transformer.ts new file mode 100644 index 0000000..67d5865 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-payload-transformer.ts @@ -0,0 +1,51 @@ +import { discountUtils } from "../../taxes/discount-utils"; +import { taxJarAddressFactory } from "../address-factory"; +import { Payload, Target } from "./taxjar-calculate-taxes-adapter"; + +export class TaxJarCalculateTaxesPayloadTransformer { + private mapLines(taxBase: Payload["taxBase"]): Target["params"]["line_items"] { + const { lines, discounts } = taxBase; + const discountSum = discounts?.reduce( + (total, current) => total + Number(current.amount.amount), + 0 + ); + const linePrices = lines.map((line) => Number(line.totalPrice.amount)); + const distributedDiscounts = discountUtils.distributeDiscount(discountSum, linePrices); + + const mappedLines: Target["params"]["line_items"] = lines.map((line, index) => { + const discountAmount = distributedDiscounts[index]; + + return { + id: line.sourceLine.id, + // todo: get from tax code matcher + product_tax_code: "", + quantity: line.quantity, + unit_price: Number(line.unitPrice.amount), + discount: discountAmount, + }; + }); + + return mappedLines; + } + + transform({ taxBase, channelConfig }: Payload): Target { + const fromAddress = taxJarAddressFactory.fromChannelAddress(channelConfig.address); + + if (!taxBase.address) { + throw new Error("Customer address is required to calculate taxes in TaxJar."); + } + + const toAddress = taxJarAddressFactory.fromSaleorAddress(taxBase.address); + + const taxParams: Target = { + params: { + ...fromAddress, + ...toAddress, + shipping: taxBase.shippingPrice.amount, + line_items: this.mapLines(taxBase), + }, + }; + + return taxParams; + } +} diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-lines-transformer.test.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-lines-transformer.test.ts new file mode 100644 index 0000000..e8d39f9 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-lines-transformer.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { TaxJarCalculateTaxesMockGenerator } from "./taxjar-calculate-taxes-mock-generator"; +import { matchPayloadLinesToResponseLines } from "./taxjar-calculate-taxes-response-lines-transformer"; + +describe("matchPayloadLinesToResponseLines", () => { + it("shold return the response lines in the order of payload lines", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const responseMock = mockGenerator.generateResponse(); + const payloadMock = mockGenerator.generateTaxBase(); + + const payloadLines = payloadMock.lines; + const responseLines = responseMock.tax.breakdown?.line_items ?? []; + + const [first, second, third] = matchPayloadLinesToResponseLines(payloadLines, responseLines); + + expect(first!.id).toBe(payloadLines[0].sourceLine.id); + expect(second!.id).toBe(payloadLines[1].sourceLine.id); + expect(third!.id).toBe(payloadLines[2].sourceLine.id); + }); + it("throws error if there is no match for a payload line in response lines", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const responseMock = mockGenerator.generateResponse(); + const payloadMock = mockGenerator.generateTaxBase(); + + const payloadLines = payloadMock.lines; + const responseLines = (responseMock.tax.breakdown?.line_items ?? []).slice(0, 2); + + expect(() => matchPayloadLinesToResponseLines(payloadLines, responseLines)).toThrowError( + `Saleor product line with id ${payloadLines[2].sourceLine.id} not found in TaxJar response.` + ); + }); +}); diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-lines-transformer.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-lines-transformer.ts new file mode 100644 index 0000000..3b9c26a --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-lines-transformer.ts @@ -0,0 +1,59 @@ +import Breakdown from "taxjar/dist/types/breakdown"; +import { TaxForOrderRes } from "taxjar/dist/types/returnTypes"; +import { TaxBaseFragment } from "../../../../generated/graphql"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; +import { Payload, Response } from "./taxjar-calculate-taxes-adapter"; + +/* + * TaxJar doesn't guarantee the order of the response items to match the payload items order. + * The order needs to be maintained because the response items are by it in Saleor. + */ +export function matchPayloadLinesToResponseLines( + payloadLines: TaxBaseFragment["lines"], + responseLines: NonNullable +) { + 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.` + ); + } + + return responseLine; + }); +} + +export class TaxJarCalculateTaxesResponseLinesTransformer { + transform(payload: Payload, response: TaxForOrderRes): Response["lines"] { + const responseLines = response.tax.breakdown?.line_items ?? []; + + const lines = matchPayloadLinesToResponseLines(payload.taxBase.lines, responseLines); + + return lines.map((line) => { + const taxableAmount = taxProviderUtils.resolveOptionalOrThrow( + line?.taxable_amount, + new Error("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") + ); + const taxRate = taxProviderUtils.resolveOptionalOrThrow( + line?.combined_tax_rate, + new Error("Line combined tax rate is required to calculate net amount") + ); + + return { + total_gross_amount: payload.taxBase.pricesEnteredWithTax + ? taxableAmount + : taxableAmount + taxCollectable, + total_net_amount: payload.taxBase.pricesEnteredWithTax + ? taxableAmount - taxCollectable + : taxableAmount, + tax_rate: taxRate, + }; + }); + } +} diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-shipping-transformer.test.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-shipping-transformer.test.ts new file mode 100644 index 0000000..c422452 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-shipping-transformer.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { TaxJarCalculateTaxesMockGenerator } from "./taxjar-calculate-taxes-mock-generator"; +import { TaxJarCalculateTaxesResponseShippingTransformer } from "./taxjar-calculate-taxes-response-shipping-transformer"; + +const transformer = new TaxJarCalculateTaxesResponseShippingTransformer(); + +describe("TaxJarCalculateTaxesResponseShippingTransformer", () => { + it("returns shipping with taxes", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const response = mockGenerator.generateResponse(); + const taxBase = mockGenerator.generateTaxBase(); + const result = transformer.transform(taxBase, response); + + expect(result).toEqual({ + shipping_price_gross_amount: 59.17, + shipping_price_net_amount: 54.21, + shipping_tax_rate: 0.08375, + }); + }); + it("returns no taxes when shipping not taxable", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const response = mockGenerator.generateResponse(); + const payload = mockGenerator.generateTaxBase(); + + response.tax.breakdown!.shipping = undefined; + response.tax.freight_taxable = false; + + const result = transformer.transform(payload, response); + + expect(result).toEqual({ + shipping_price_net_amount: 59.17, + shipping_price_gross_amount: 59.17, + shipping_tax_rate: 0, + }); + }); + it("returns gross amount reduced by tax when pricesEnteredWithTax = true", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const response = mockGenerator.generateResponse(); + const payload = mockGenerator.generateTaxBase(); + + const result = transformer.transform(payload, response); + + expect(result).toEqual({ + shipping_price_gross_amount: 59.17, + shipping_price_net_amount: 54.21, + shipping_tax_rate: 0.08375, + }); + }); + it("returns gross amount when pricesEnteredWithTax = false", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_excluded"); + const response = mockGenerator.generateResponse(); + const payload = mockGenerator.generateTaxBase(); + + const result = transformer.transform(payload, response); + + expect(result).toEqual({ + shipping_price_gross_amount: 64.13, + shipping_price_net_amount: 59.17, + shipping_tax_rate: 0.08375, + }); + }); +}); diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-shipping-transformer.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-shipping-transformer.ts new file mode 100644 index 0000000..ae388a1 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-shipping-transformer.ts @@ -0,0 +1,47 @@ +import { TaxForOrderRes } from "taxjar/dist/types/returnTypes"; +import { numbers } from "../../taxes/numbers"; +import { Payload, Response } from "./taxjar-calculate-taxes-adapter"; + +export class TaxJarCalculateTaxesResponseShippingTransformer { + transform( + taxBase: Payload["taxBase"], + res: TaxForOrderRes + ): Pick< + Response, + "shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate" + > { + const { tax } = res; + + /* + * If the shipping is not taxable, we return the same values as in the payload. + * If freight_taxable = true, tax.breakdown.shipping exists + */ + if (!tax.freight_taxable) { + return { + shipping_price_gross_amount: tax.shipping, + shipping_price_net_amount: tax.shipping, + shipping_tax_rate: 0, + }; + } + + const isTaxIncluded = taxBase.pricesEnteredWithTax; + + const shippingDetails = tax.breakdown!.shipping!; + const shippingTaxableAmount = shippingDetails.taxable_amount; + const shippingTaxCollectable = shippingDetails.tax_collectable; + + const shippingPriceGross = isTaxIncluded + ? shippingTaxableAmount + : shippingTaxableAmount + shippingTaxCollectable; + const shippingPriceNet = isTaxIncluded + ? shippingTaxableAmount - shippingTaxCollectable + : shippingTaxableAmount; + const shippingTaxRate = shippingDetails.combined_tax_rate; + + return { + shipping_price_gross_amount: numbers.roundFloatToTwoDecimals(shippingPriceGross), + shipping_price_net_amount: numbers.roundFloatToTwoDecimals(shippingPriceNet), + shipping_tax_rate: shippingTaxRate, + }; + } +} diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-transformer.test.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-transformer.test.ts new file mode 100644 index 0000000..db0e519 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-transformer.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; +import { TaxJarCalculateTaxesMockGenerator } from "./taxjar-calculate-taxes-mock-generator"; +import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer"; + +const transformer = new TaxJarCalculateTaxesResponseTransformer(); + +describe("TaxJarCalculateTaxesResponseTransformer", () => { + it("returns values from payload if no nexus", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_no_nexus_tax_included"); + const noNexusResponseMock = mockGenerator.generateResponse(); + const payloadMock = { + taxBase: mockGenerator.generateTaxBase(), + channelConfig: mockGenerator.generateChannelConfig(), + }; + + const result = transformer.transform(payloadMock, noNexusResponseMock); + + expect(result).toEqual({ + shipping_price_net_amount: 59.17, + shipping_price_gross_amount: 59.17, + shipping_tax_rate: 0, + lines: [ + { + total_gross_amount: 60, + total_net_amount: 60, + tax_rate: 0, + }, + { + total_gross_amount: 20, + total_net_amount: 20, + tax_rate: 0, + }, + { + total_gross_amount: 100, + total_net_amount: 100, + tax_rate: 0, + }, + ], + }); + }); + it("transforms response when nexus is found", () => { + const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included"); + const nexusResponse = mockGenerator.generateResponse(); + + const payloadMock = { + taxBase: mockGenerator.generateTaxBase(), + channelConfig: mockGenerator.generateChannelConfig(), + }; + + const result = transformer.transform(payloadMock, nexusResponse); + + expect(result).toEqual({ + shipping_price_gross_amount: 59.17, + shipping_price_net_amount: 54.21, + shipping_tax_rate: 0.08375, + lines: [ + { + total_gross_amount: 20, + total_net_amount: 18.32, + tax_rate: 0.08375, + }, + { + total_gross_amount: 100, + total_net_amount: 91.62, + tax_rate: 0.08375, + }, + { + total_gross_amount: 60, + total_net_amount: 54.97, + tax_rate: 0.08375, + }, + ], + }); + }); +}); diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-transformer.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-transformer.ts new file mode 100644 index 0000000..8a0523c --- /dev/null +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-response-transformer.ts @@ -0,0 +1,46 @@ +import { TaxForOrderRes } from "taxjar/dist/types/returnTypes"; +import { Logger, createLogger } from "../../../lib/logger"; +import { Payload, Response } from "./taxjar-calculate-taxes-adapter"; +import { TaxJarCalculateTaxesResponseLinesTransformer } from "./taxjar-calculate-taxes-response-lines-transformer"; +import { TaxJarCalculateTaxesResponseShippingTransformer } from "./taxjar-calculate-taxes-response-shipping-transformer"; + +export class TaxJarCalculateTaxesResponseTransformer { + private logger: Logger; + + constructor() { + this.logger = createLogger({ name: "TaxJarCalculateTaxesResponseTransformer" }); + } + + transform(payload: Payload, response: TaxForOrderRes): Response { + /* + * TaxJar operates on the idea of sales tax nexus. Nexus is a place where the company has a physical presence. + * If the company has no nexus in the state where the customer is located, the company is not required to collect sales tax. + * Therefore, if has_nexus = false, we don't calculate taxes and return the same values as in the payload. + * See: https://www.taxjar.com/sales-tax/nexus + */ + if (!response.tax.has_nexus) { + this.logger.warn("The company has no nexus in the state where the customer is located"); + return { + shipping_price_net_amount: payload.taxBase.shippingPrice.amount, + shipping_price_gross_amount: payload.taxBase.shippingPrice.amount, + shipping_tax_rate: 0, + lines: payload.taxBase.lines.map((line) => ({ + total_gross_amount: line.totalPrice.amount, + total_net_amount: line.totalPrice.amount, + tax_rate: 0, + })), + }; + } + + const shippingTransformer = new TaxJarCalculateTaxesResponseShippingTransformer(); + const linesTransformer = new TaxJarCalculateTaxesResponseLinesTransformer(); + + const shipping = shippingTransformer.transform(payload.taxBase, response); + const lines = linesTransformer.transform(payload, response); + + return { + ...shipping, + lines, + }; + } +} diff --git a/apps/taxes/src/modules/taxjar/maps/address-factory.ts b/apps/taxes/src/modules/taxjar/maps/address-factory.ts deleted file mode 100644 index bdf3f59..0000000 --- a/apps/taxes/src/modules/taxjar/maps/address-factory.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ChannelAddress } from "../../channels-configuration/channels-config"; -import { AddressFragment as SaleorAddress } from "../../../../generated/graphql"; -import { AddressParams as TaxJarAddress } from "taxjar/dist/types/paramTypes"; - -function joinAddresses(address1: string, address2: string): string { - return `${address1}${address2.length > 0 ? " " + address2 : ""}`; -} - -function mapSaleorAddressToTaxJarAddress(address: SaleorAddress): TaxJarAddress { - return { - street: joinAddresses(address.streetAddress1, address.streetAddress2), - city: address.city, - zip: address.postalCode, - state: address.countryArea, - country: address.country.code, - }; -} - -function mapChannelAddressToTaxJarAddress(address: ChannelAddress): TaxJarAddress { - return { - city: address.city, - country: address.country, - state: address.state, - street: address.street, - zip: address.zip, - }; -} - -export const taxJarAddressFactory = { - fromSaleorAddress: mapSaleorAddressToTaxJarAddress, - fromChannelAddress: mapChannelAddressToTaxJarAddress, -}; diff --git a/apps/taxes/src/modules/taxjar/maps/taxjar-calculate-taxes-map.test.ts b/apps/taxes/src/modules/taxjar/maps/taxjar-calculate-taxes-map.test.ts deleted file mode 100644 index 55b8f59..0000000 --- a/apps/taxes/src/modules/taxjar/maps/taxjar-calculate-taxes-map.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, it } from "vitest"; - -describe.skip("taxJarCalculateTaxesMaps", () => { - describe.todo("mapResponse", () => { - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - }); - describe.todo("mapPayload", () => { - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - }); -}); diff --git a/apps/taxes/src/modules/taxjar/maps/taxjar-calculate-taxes-map.ts b/apps/taxes/src/modules/taxjar/maps/taxjar-calculate-taxes-map.ts deleted file mode 100644 index aaa7e6e..0000000 --- a/apps/taxes/src/modules/taxjar/maps/taxjar-calculate-taxes-map.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { TaxForOrderRes } from "taxjar/dist/types/returnTypes"; -import { TaxBaseFragment, TaxBaseLineFragment } from "../../../../generated/graphql"; -import { ChannelConfig } from "../../channels-configuration/channels-config"; -import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook"; -import { FetchTaxForOrderArgs } from "../taxjar-client"; - -function getTaxBaseLineDiscount( - line: TaxBaseLineFragment, - totalDiscount: number, - allLinesTotal: number -) { - if (totalDiscount === 0 || allLinesTotal === 0) { - return 0; - } - const lineTotalAmount = Number(line.totalPrice.amount); - const discountAmount = (lineTotalAmount / allLinesTotal) * totalDiscount; - - if (discountAmount > lineTotalAmount) { - return lineTotalAmount; - } - return discountAmount; -} - -const formatCalculatedAmount = (amount: number) => { - return Number(amount.toFixed(2)); -}; - -// * This type is related to `TaxLineItem` from TaxJar. It should be unified. -type FetchTaxesLinePayload = { - id: string; - quantity: number; - taxCode?: string | null; - discount: number; - chargeTaxes: boolean; - unitAmount: number; - totalAmount: number; -}; - -const prepareLinesWithDiscountPayload = ( - taxBase: TaxBaseFragment -): Array => { - const { lines, discounts } = taxBase; - const allLinesTotal = lines.reduce( - (total, current) => total + Number(current.totalPrice.amount), - 0 - ); - const discountsSum = - discounts?.reduce((total, current) => total + Number(current.amount.amount), 0) || 0; - - // Make sure that totalDiscount doesn't exceed a sum of all lines - const totalDiscount = discountsSum <= allLinesTotal ? discountsSum : allLinesTotal; - - return lines.map((line) => { - const discountAmount = getTaxBaseLineDiscount(line, totalDiscount, allLinesTotal); - - return { - id: line.sourceLine.id, - chargeTaxes: taxBase.pricesEnteredWithTax, - // todo: get from tax code matcher - taxCode: "", - quantity: line.quantity, - totalAmount: Number(line.totalPrice.amount), - unitAmount: Number(line.unitPrice.amount), - discount: discountAmount, - }; - }); -}; - -const mapResponse = ( - payload: TaxBaseFragment, - response: TaxForOrderRes -): CalculateTaxesResponse => { - const linesWithDiscount = prepareLinesWithDiscountPayload(payload); - const linesWithChargeTaxes = linesWithDiscount.filter((line) => line.chargeTaxes === true); - - const taxResponse = linesWithChargeTaxes.length !== 0 ? response : undefined; - const taxDetails = taxResponse?.tax.breakdown; - /** - * todo: investigate - * ! There is no shipping in tax.breakdown from TaxJar. - */ - const shippingDetails = taxDetails?.shipping; - - const shippingPriceGross = shippingDetails - ? shippingDetails.taxable_amount + shippingDetails.tax_collectable - : payload.shippingPrice.amount; - const shippingPriceNet = shippingDetails - ? shippingDetails.taxable_amount - : payload.shippingPrice.amount; - const shippingTaxRate = shippingDetails ? shippingDetails.combined_tax_rate : 0; - // ! It appears shippingTaxRate is always 0 from TaxJar. - - return { - shipping_price_gross_amount: formatCalculatedAmount(shippingPriceGross), - shipping_price_net_amount: formatCalculatedAmount(shippingPriceNet), - shipping_tax_rate: shippingTaxRate, - /** - * lines order needs to be the same as for received payload. - * lines that have chargeTaxes === false will have returned default value - */ - lines: linesWithDiscount.map((line) => { - const lineTax = taxDetails?.line_items?.find((l) => l.id === line.id); - const totalGrossAmount = lineTax - ? lineTax.taxable_amount + lineTax.tax_collectable - : line.totalAmount - line.discount; - const totalNetAmount = lineTax ? lineTax.taxable_amount : line.totalAmount - line.discount; - const taxRate = lineTax ? lineTax.combined_tax_rate : 0; - - return { - total_gross_amount: formatCalculatedAmount(totalGrossAmount), - total_net_amount: formatCalculatedAmount(totalNetAmount), - tax_rate: taxRate ?? 0, - }; - }), - }; -}; - -const mapPayload = (taxBase: TaxBaseFragment, channel: ChannelConfig): FetchTaxForOrderArgs => { - const linesWithDiscount = prepareLinesWithDiscountPayload(taxBase); - const linesWithChargeTaxes = linesWithDiscount.filter((line) => line.chargeTaxes === true); - - const taxParams = { - params: { - from_country: channel.address.country, - from_zip: channel.address.zip, - from_state: channel.address.state, - from_city: channel.address.city, - from_street: channel.address.street, - to_country: taxBase.address!.country.code, - to_zip: taxBase.address!.postalCode, - to_state: taxBase.address!.countryArea, - to_city: taxBase.address!.city, - to_street: `${taxBase.address!.streetAddress1} ${taxBase.address!.streetAddress2}`, - shipping: taxBase.shippingPrice.amount, - line_items: linesWithChargeTaxes.map((line) => ({ - id: line.id, - quantity: line.quantity, - product_tax_code: line.taxCode || undefined, - unit_price: line.unitAmount, - discount: line.discount, - })), - }, - }; - - return taxParams; -}; - -export const taxJarCalculateTaxesMaps = { - mapPayload, - mapResponse, -}; diff --git a/apps/taxes/src/modules/taxjar/maps/taxjar-order-created-map.test.ts b/apps/taxes/src/modules/taxjar/maps/taxjar-order-created-map.test.ts deleted file mode 100644 index 8be2845..0000000 --- a/apps/taxes/src/modules/taxjar/maps/taxjar-order-created-map.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { OrderStatus } from "../../../../generated/graphql"; -import { - TaxJarOrderCreatedMapPayloadArgs, - taxJarOrderCreatedMaps, -} from "./taxjar-order-created-map"; - -const MOCKED_ORDER: TaxJarOrderCreatedMapPayloadArgs = { - order: { - id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5", - created: "2023-04-11T11:03:09.304109+00:00", - status: OrderStatus.Unfulfilled, - user: { - id: "VXNlcjo5ZjY3ZjY0Zi1iZjY5LTQ5ZjYtYjQ4Zi1iZjY3ZjY0ZjY0ZjY=", - email: "tester@saleor.io", - }, - channel: { - id: "Q2hhbm5lbDoy", - slug: "channel-pln", - }, - shippingAddress: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - countryArea: "CA", - postalCode: "90002", - country: { - code: "US", - }, - }, - billingAddress: { - streetAddress1: "123 Palm Grove Ln", - streetAddress2: "", - city: "LOS ANGELES", - countryArea: "CA", - postalCode: "90002", - country: { - code: "US", - }, - }, - total: { - net: { - amount: 183.33, - }, - tax: { - amount: 12.83, - }, - currency: "USD", - }, - shippingPrice: { - gross: { - amount: 48.33, - }, - net: { - amount: 43.74, - }, - }, - lines: [ - { - productSku: "328223581", - productName: "Monospace Tee", - quantity: 1, - unitPrice: { - net: { - amount: 90, - }, - }, - totalPrice: { - net: { - amount: 90, - }, - tax: { - amount: 8.55, - }, - }, - }, - { - productSku: "328223580", - productName: "Monospace Tee", - quantity: 1, - unitPrice: { - net: { - amount: 45, - }, - }, - totalPrice: { - net: { - amount: 45, - }, - tax: { - amount: 4.28, - }, - }, - }, - ], - discounts: [ - { - amount: { - amount: 10, - }, - id: "RGlzY291bnREaXNjb3VudDox", - }, - ], - }, - channel: { - providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081", - enabled: false, - address: { - country: "US", - zip: "92093", - state: "CA", - city: "La Jolla", - street: "9500 Gilman Drive", - }, - }, -}; - -describe("taxJarOrderCreatedMaps", () => { - describe("mapPayload", () => { - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - it("returns the correct order amount", () => { - const result = taxJarOrderCreatedMaps.mapPayload(MOCKED_ORDER); - - expect(result.params.amount).toBe(183.33); - }); - }); - - describe.todo("mapResponse", () => { - it.todo("calculation of fields"); - it.todo("formatting the fields"); - it.todo("rounding of numbers"); - }); - - describe("sumLines", () => { - it("returns the sum of all line items when items quantity = 1", () => { - const result = taxJarOrderCreatedMaps.sumLines([ - { - quantity: 1, - unit_price: 90.45, - product_identifier: "328223581", - }, - { - quantity: 1, - unit_price: 45.25, - product_identifier: "328223580", - }, - ]); - - expect(result).toBe(135.7); - }); - it("returns the sum of all line items when items quantity > 1", () => { - const result = taxJarOrderCreatedMaps.sumLines([ - { - quantity: 3, - unit_price: 90.45, - product_identifier: "328223581", - }, - { - quantity: 2, - unit_price: 45.25, - product_identifier: "328223580", - }, - { - quantity: 1, - unit_price: 50.25, - product_identifier: "328223580", - }, - ]); - - expect(result).toBe(412.1); - }); - }); -}); diff --git a/apps/taxes/src/modules/taxjar/maps/taxjar-order-created-map.ts b/apps/taxes/src/modules/taxjar/maps/taxjar-order-created-map.ts deleted file mode 100644 index e44d5f7..0000000 --- a/apps/taxes/src/modules/taxjar/maps/taxjar-order-created-map.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { LineItem } from "taxjar/dist/types/paramTypes"; -import { CreateOrderRes } from "taxjar/dist/types/returnTypes"; -import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; -import { ChannelConfig } from "../../channels-configuration/channels-config"; -import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; -import { CreateOrderArgs } from "../taxjar-client"; -import { numbers } from "../../taxes/numbers"; -import { taxProviderUtils } from "../../taxes/tax-provider-utils"; - -function mapLines(lines: OrderCreatedSubscriptionFragment["lines"]): LineItem[] { - return lines.map((line) => ({ - quantity: line.quantity, - unit_price: line.unitPrice.net.amount, - product_identifier: line.productSku ?? "", - // todo: add from tax code matcher - product_tax_code: "", - sales_tax: line.totalPrice.tax.amount, - description: line.productName, - })); -} - -function sumLines(lines: LineItem[]): number { - return numbers.roundFloatToTwoDecimals( - lines.reduce( - (prev, line) => - prev + - taxProviderUtils.resolveOptionalOrThrow( - line.unit_price, - new Error("line.unit_price is undefined") - ) * - taxProviderUtils.resolveOptionalOrThrow( - line.quantity, - new Error("line.quantity is undefined") - ), - 0 - ) - ); -} - -export type TaxJarOrderCreatedMapPayloadArgs = { - order: OrderCreatedSubscriptionFragment; - channel: ChannelConfig; -}; - -const mapPayload = ({ order, channel }: TaxJarOrderCreatedMapPayloadArgs): CreateOrderArgs => { - const lineItems = mapLines(order.lines); - const lineSum = sumLines(lineItems); - const shippingAmount = order.shippingPrice.gross.amount; - /** - * "The TaxJar API performs arbitrary-precision decimal arithmetic for accurately calculating sales tax." - * but we want to round to 2 decimals for consistency - */ - const orderAmount = numbers.roundFloatToTwoDecimals(shippingAmount + lineSum); - - return { - params: { - from_country: channel.address.country, - from_zip: channel.address.zip, - from_state: channel.address.state, - from_city: channel.address.city, - from_street: channel.address.street, - to_country: order.shippingAddress!.country.code, - to_zip: order.shippingAddress!.postalCode, - to_state: order.shippingAddress!.countryArea, - to_city: order.shippingAddress!.city, - to_street: `${order.shippingAddress!.streetAddress1} ${ - order.shippingAddress!.streetAddress2 - }`, - shipping: shippingAmount, - line_items: lineItems, - transaction_date: order.created, - transaction_id: order.id, - amount: orderAmount, // Total amount of the order with shipping, excluding sales tax in dollars. - // todo: add sales_tax - sales_tax: 0, - }, - }; -}; - -const mapResponse = (response: CreateOrderRes): CreateOrderResponse => { - return { - id: response.order.transaction_id, - }; -}; - -export const taxJarOrderCreatedMaps = { - mapPayload, - mapResponse, - sumLines, -}; diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-adapter.test.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-adapter.test.ts new file mode 100644 index 0000000..256bb87 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-adapter.test.ts @@ -0,0 +1,3 @@ +import { describe } from "vitest"; + +describe.todo("TaxJarOrderCreatedAdapter", () => {}); diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-adapter.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-adapter.ts new file mode 100644 index 0000000..61ca293 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-adapter.ts @@ -0,0 +1,39 @@ +import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; +import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; +import { TaxJarOrderCreatedPayloadTransformer } from "./taxjar-order-created-payload-transformer"; +import { CreateOrderArgs, TaxJarClient } from "../taxjar-client"; +import { TaxJarConfig } from "../taxjar-config"; +import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer"; +import { Logger, createLogger } from "../../../lib/logger"; + +export type Payload = { order: OrderCreatedSubscriptionFragment; channelConfig: ChannelConfig }; +export type Target = CreateOrderArgs; +type Response = CreateOrderResponse; + +export class TaxJarOrderCreatedAdapter implements WebhookAdapter { + private logger: Logger; + constructor(private readonly config: TaxJarConfig) { + this.logger = createLogger({ service: "TaxJarOrderCreatedAdapter" }); + } + + async send(payload: Payload): Promise { + this.logger.debug({ payload }, "send called with:"); + + const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer(); + const target = payloadTransformer.transform(payload); + + const client = new TaxJarClient(this.config); + const response = await client.createOrder(target); + + this.logger.debug({ response }, "TaxJar createOrder response:"); + + const responseTransformer = new TaxJarOrderCreatedResponseTransformer(); + const transformedResponse = responseTransformer.transform(response); + + this.logger.debug({ transformedResponse }, "Transformed TaxJar createOrder response to:"); + + return transformedResponse; + } +} diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-mock-generator.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-mock-generator.ts new file mode 100644 index 0000000..35f9cb5 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-mock-generator.ts @@ -0,0 +1,105 @@ +import { CreateOrderRes } from "taxjar/dist/types/returnTypes"; +import { OrderCreatedSubscriptionFragment, OrderStatus } from "../../../../generated/graphql"; +import { ChannelConfig } from "../../channels-configuration/channels-config"; +import { defaultOrder } from "../../../mocks"; + +type Order = OrderCreatedSubscriptionFragment; + +const defaultChannelConfig: ChannelConfig = { + providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50", + enabled: false, + address: { + country: "US", + zip: "95008", + state: "CA", + city: "Campbell", + street: "33 N. First Street", + }, +}; + +const defaultOrderCreatedResponse: CreateOrderRes = { + order: { + user_id: 314973, + transaction_reference_id: null, + transaction_id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2", + transaction_date: "2023-05-25T09:18:55.203Z", + to_zip: "94111", + to_street: "600 Montgomery St", + to_state: "CA", + to_country: "US", + to_city: "SAN FRANCISCO", + shipping: 59.17, + sales_tax: 0.0, + provider: "api", + line_items: [ + { + unit_price: 20.0, + sales_tax: 5.18, + quantity: 3, + product_tax_code: "", + product_identifier: "328223580", + id: "0", + discount: 0.0, + description: "Monospace Tee", + }, + { + unit_price: 20.0, + sales_tax: 1.73, + quantity: 1, + product_tax_code: "", + product_identifier: "328223581", + id: "1", + discount: 0.0, + description: "Monospace Tee", + }, + { + unit_price: 50.0, + sales_tax: 8.63, + quantity: 2, + product_tax_code: "", + product_identifier: "118223581", + id: "2", + discount: 0.0, + description: "Paul's Balance 420", + }, + ], + from_zip: "95008", + from_street: "33 N. First Street", + from_state: "CA", + from_country: "US", + from_city: "CAMPBELL", + exemption_type: null, + amount: 239.17, + }, +}; + +const testingScenariosMap = { + default: { + order: defaultOrder, + channelConfig: defaultChannelConfig, + response: defaultOrderCreatedResponse, + }, +}; + +type TestingScenario = keyof typeof testingScenariosMap; + +export class TaxJarOrderCreatedMockGenerator { + constructor(private scenario: TestingScenario = "default") {} + generateOrder = (overrides: Partial = {}): Order => + structuredClone({ + ...testingScenariosMap[this.scenario].order, + ...overrides, + }); + + generateChannelConfig = (overrides: Partial = {}): ChannelConfig => + structuredClone({ + ...testingScenariosMap[this.scenario].channelConfig, + ...overrides, + }); + + generateResponse = (overrides: Partial = {}): CreateOrderRes => + structuredClone({ + ...testingScenariosMap[this.scenario].response, + ...overrides, + }); +} diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-payload-transformer.test.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-payload-transformer.test.ts new file mode 100644 index 0000000..e94484d --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-payload-transformer.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { TaxJarOrderCreatedMockGenerator } from "./taxjar-order-created-mock-generator"; +import { + TaxJarOrderCreatedPayloadTransformer, + sumPayloadLines, +} from "./taxjar-order-created-payload-transformer"; + +const mockGenerator = new TaxJarOrderCreatedMockGenerator(); + +describe("TaxJarOrderCreatedPayloadTransformer", () => { + it("returns the correct order amount", () => { + const payloadMock = { + order: mockGenerator.generateOrder(), + channelConfig: mockGenerator.generateChannelConfig(), + }; + const transformer = new TaxJarOrderCreatedPayloadTransformer(); + const transformedPayload = transformer.transform(payloadMock); + + expect(transformedPayload.params.amount).toBe(239.17); + }); +}); + +describe("sumPayloadLines", () => { + it("returns the sum of all line items when items quantity = 1", () => { + const result = sumPayloadLines([ + { + quantity: 1, + unit_price: 90.45, + product_identifier: "328223581", + }, + { + quantity: 1, + unit_price: 45.25, + product_identifier: "328223580", + }, + ]); + + expect(result).toBe(135.7); + }); + it("returns the sum of all line items when items quantity > 1", () => { + const result = sumPayloadLines([ + { + quantity: 3, + unit_price: 90.45, + product_identifier: "328223581", + }, + { + quantity: 2, + unit_price: 45.25, + product_identifier: "328223580", + }, + { + quantity: 1, + unit_price: 50.25, + product_identifier: "328223580", + }, + ]); + + expect(result).toBe(412.1); + }); +}); diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-payload-transformer.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-payload-transformer.ts new file mode 100644 index 0000000..a4ccf54 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-payload-transformer.ts @@ -0,0 +1,73 @@ +import { LineItem } from "taxjar/dist/util/types"; +import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; +import { numbers } from "../../taxes/numbers"; +import { Payload, Target } from "./taxjar-order-created-adapter"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; + +export function sumPayloadLines(lines: LineItem[]): number { + return numbers.roundFloatToTwoDecimals( + lines.reduce( + (prev, line) => + prev + + taxProviderUtils.resolveOptionalOrThrow( + line.unit_price, + new Error("Line unit_price is required to calculate order taxes") + ) * + taxProviderUtils.resolveOptionalOrThrow( + line.quantity, + new Error("Line quantity is required to calculate order taxes") + ), + 0 + ) + ); +} + +export class TaxJarOrderCreatedPayloadTransformer { + private mapLines(lines: OrderCreatedSubscriptionFragment["lines"]): LineItem[] { + return lines.map((line) => ({ + quantity: line.quantity, + unit_price: line.unitPrice.net.amount, + product_identifier: line.productSku ?? "", + // todo: add from tax code matcher + product_tax_code: "", + sales_tax: line.totalPrice.tax.amount, + description: line.productName, + })); + } + + transform({ order, channelConfig }: Payload): Target { + const lineItems = this.mapLines(order.lines); + const lineSum = sumPayloadLines(lineItems); + const shippingAmount = order.shippingPrice.gross.amount; + /** + * "The TaxJar API performs arbitrary-precision decimal arithmetic for accurately calculating sales tax." + * but we want to round to 2 decimals for consistency + */ + const orderAmount = numbers.roundFloatToTwoDecimals(shippingAmount + lineSum); + + return { + params: { + from_country: channelConfig.address.country, + from_zip: channelConfig.address.zip, + from_state: channelConfig.address.state, + from_city: channelConfig.address.city, + from_street: channelConfig.address.street, + to_country: order.shippingAddress!.country.code, + to_zip: order.shippingAddress!.postalCode, + to_state: order.shippingAddress!.countryArea, + to_city: order.shippingAddress!.city, + to_street: `${order.shippingAddress!.streetAddress1} ${ + order.shippingAddress!.streetAddress2 + }`, + shipping: shippingAmount, + line_items: lineItems, + transaction_date: order.created, + transaction_id: order.id, + amount: orderAmount, + + // todo: add sales_tax + sales_tax: 0, + }, + }; + } +} diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-response-transformer.test.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-response-transformer.test.ts new file mode 100644 index 0000000..bbe9a66 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-response-transformer.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { TaxJarOrderCreatedMockGenerator } from "./taxjar-order-created-mock-generator"; +import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer"; + +describe("TaxJarOrderCreatedResponseTransformer", () => { + it("returns orded id in response", () => { + const mockGenerator = new TaxJarOrderCreatedMockGenerator(); + const responseMock = mockGenerator.generateResponse(); + const transformer = new TaxJarOrderCreatedResponseTransformer(); + const result = transformer.transform(responseMock); + + expect(result).toEqual({ + id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2", + }); + }); +}); diff --git a/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-response-transformer.ts b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-response-transformer.ts new file mode 100644 index 0000000..3022785 --- /dev/null +++ b/apps/taxes/src/modules/taxjar/order-created/taxjar-order-created-response-transformer.ts @@ -0,0 +1,10 @@ +import { CreateOrderRes } from "taxjar/dist/types/returnTypes"; +import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; + +export class TaxJarOrderCreatedResponseTransformer { + transform(response: CreateOrderRes): CreateOrderResponse { + return { + id: response.order.transaction_id, + }; + } +} diff --git a/apps/taxes/src/modules/taxjar/taxjar-client.ts b/apps/taxes/src/modules/taxjar/taxjar-client.ts index fa8e36b..8537173 100644 --- a/apps/taxes/src/modules/taxjar/taxjar-client.ts +++ b/apps/taxes/src/modules/taxjar/taxjar-client.ts @@ -39,14 +39,14 @@ export class TaxJarClient { } async fetchTaxForOrder({ params }: FetchTaxForOrderArgs) { - this.logger.debug({ params }, "fetchTaxForOrder called with:"); + this.logger.trace({ params }, "fetchTaxForOrder called with:"); const response = await this.client.taxForOrder(params); return response; } async ping() { - this.logger.debug("ping called"); + this.logger.trace("ping called"); try { await this.client.categories(); return { authenticated: true }; @@ -59,13 +59,13 @@ export class TaxJarClient { } async createOrder({ params }: CreateOrderArgs) { - this.logger.debug({ params }, "createOrder called with:"); + this.logger.trace({ params }, "createOrder called with:"); return this.client.createOrder(params); } async validateAddress({ params }: ValidateAddressArgs) { - this.logger.debug({ params }, "validateAddress called with:"); + this.logger.trace({ params }, "validateAddress called with:"); return this.client.validateAddress(params); } diff --git a/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts b/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts index 0f0de5d..55c8067 100644 --- a/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts +++ b/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts @@ -1,43 +1,46 @@ import { OrderCreatedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql"; -import { createLogger, Logger } from "../../lib/logger"; +import { Logger, createLogger } from "../../lib/logger"; import { ChannelConfig } from "../channels-configuration/channels-config"; import { ProviderWebhookService } from "../taxes/tax-provider-webhook"; +import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter"; import { TaxJarClient } from "./taxjar-client"; import { TaxJarConfig } from "./taxjar-config"; -import { taxJarCalculateTaxesMaps } from "./maps/taxjar-calculate-taxes-map"; -import { taxJarOrderCreatedMaps } from "./maps/taxjar-order-created-map"; +import { TaxJarOrderCreatedAdapter } from "./order-created/taxjar-order-created-adapter"; export class TaxJarWebhookService implements ProviderWebhookService { client: TaxJarClient; + config: TaxJarConfig; private logger: Logger; constructor(config: TaxJarConfig) { - const avataxClient = new TaxJarClient(config); + const taxJarClient = new TaxJarClient(config); - this.client = avataxClient; + this.client = taxJarClient; + this.config = config; this.logger = createLogger({ - service: "TaxJarProvider", + service: "TaxJarWebhookService", }); } - async calculateTaxes(payload: TaxBaseFragment, channel: ChannelConfig) { - this.logger.debug({ payload, channel }, "calculateTaxes called with:"); - const args = taxJarCalculateTaxesMaps.mapPayload(payload, channel); - const fetchedTaxes = await this.client.fetchTaxForOrder(args); + async calculateTaxes(taxBase: TaxBaseFragment, channelConfig: ChannelConfig) { + this.logger.debug({ taxBase, channelConfig }, "calculateTaxes called with:"); + const adapter = new TaxJarCalculateTaxesAdapter(this.config); - this.logger.debug({ fetchedTaxes }, "fetchTaxForOrder response"); + const response = await adapter.send({ channelConfig, taxBase }); - return taxJarCalculateTaxesMaps.mapResponse(payload, fetchedTaxes); + this.logger.debug({ response }, "calculateTaxes response:"); + return response; } - async createOrder(order: OrderCreatedSubscriptionFragment, channel: ChannelConfig) { - this.logger.debug({ order, channel }, "createOrder called with:"); - const args = taxJarOrderCreatedMaps.mapPayload({ order, channel }); - const result = await this.client.createOrder(args); + async createOrder(order: OrderCreatedSubscriptionFragment, channelConfig: ChannelConfig) { + this.logger.debug({ order, channelConfig }, "createOrder called with:"); - this.logger.debug({ createOrder: result }, "createOrder response"); + const adapter = new TaxJarOrderCreatedAdapter(this.config); - return taxJarOrderCreatedMaps.mapResponse(result); + const response = await adapter.send({ channelConfig, order }); + + this.logger.debug({ response }, "createOrder response:"); + return response; } // * TaxJar doesn't require any action on order fulfillment diff --git a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts index 5e55d27..4dbbd54 100644 --- a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -28,15 +28,6 @@ function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) { return payload; } -// ? maybe make it a part of WebhookResponse? -function handleWebhookError(error: unknown) { - const logger = createLogger({ service: "checkout-calculate-taxes", name: "handleWebhookError" }); - - if (error instanceof Error) { - logger.error(error.stack); - } -} - export const checkoutCalculateTaxesSyncWebhook = new SaleorSyncWebhook({ name: "CheckoutCalculateTaxes", apl: saleorApp.apl, @@ -57,27 +48,20 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res, logger.info("Payload validated succesfully"); } catch (error) { logger.info("Returning no data"); - return webhookResponse.failure("Payload is invalid"); + return webhookResponse.error(error); } try { const appMetadata = payload.recipient?.privateMetadata ?? []; const channelSlug = payload.taxBase.channel.slug; - const activeTaxProvider = getActiveTaxProvider(channelSlug, appMetadata); + const taxProvider = getActiveTaxProvider(channelSlug, appMetadata); - if (!activeTaxProvider.ok) { - logger.info("Returning no data"); - return webhookResponse.failure(activeTaxProvider.error); - } - - logger.info({ activeTaxProvider }, "Fetched activeTaxProvider"); - const taxProvider = activeTaxProvider.data; + logger.info({ taxProvider }, "Fetched taxProvider"); const calculatedTaxes = await taxProvider.calculateTaxes(payload.taxBase); logger.info({ calculatedTaxes }, "Taxes calculated"); return webhookResponse.success(ctx.buildResponse(calculatedTaxes)); } catch (error) { - handleWebhookError(error); - return webhookResponse.failure("Error while calculating taxes"); + return webhookResponse.error(error); } }); diff --git a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts index dec830c..6e95da3 100644 --- a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts @@ -28,15 +28,6 @@ function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) { return payload; } -// ? maybe make it a part of WebhookResponse? -function handleWebhookError(error: unknown) { - const logger = createLogger({ service: "order-calculate-taxes", name: "handleWebhookError" }); - - if (error instanceof Error) { - logger.error(error.stack); - } -} - export const orderCalculateTaxesSyncWebhook = new SaleorSyncWebhook({ name: "OrderCalculateTaxes", apl: saleorApp.apl, @@ -56,28 +47,21 @@ export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx verifyCalculateTaxesPayload(payload); logger.info("Payload validated succesfully"); } catch (error) { - logger.info("Returning no data"); - return webhookResponse.failure("Payload is invalid"); + logger.info("Payload is invalid. Returning no data"); + return webhookResponse.error(error); } try { const appMetadata = payload.recipient?.privateMetadata ?? []; const channelSlug = payload.taxBase.channel.slug; - const activeTaxProvider = getActiveTaxProvider(channelSlug, appMetadata); + const taxProvider = getActiveTaxProvider(channelSlug, appMetadata); - if (!activeTaxProvider.ok) { - logger.info("Returning no data"); - return webhookResponse.failure(activeTaxProvider.error); - } - - logger.info({ activeTaxProvider }, "Fetched activeTaxProvider"); - const taxProvider = activeTaxProvider.data; + logger.info({ taxProvider }, "Fetched taxProvider"); const calculatedTaxes = await taxProvider.calculateTaxes(payload.taxBase); logger.info({ calculatedTaxes }, "Taxes calculated"); return webhookResponse.success(ctx.buildResponse(calculatedTaxes)); } catch (error) { - handleWebhookError(error); - return webhookResponse.failure("Error while calculating taxes"); + return webhookResponse.error(error); } }); diff --git a/apps/taxes/src/pages/api/webhooks/order-created.ts b/apps/taxes/src/pages/api/webhooks/order-created.ts index ef718c7..93aa437 100644 --- a/apps/taxes/src/pages/api/webhooks/order-created.ts +++ b/apps/taxes/src/pages/api/webhooks/order-created.ts @@ -13,7 +13,7 @@ import { getActiveTaxProvider } from "../../../modules/taxes/active-tax-provider import { createClient } from "../../../lib/graphql"; import { Client } from "urql"; import { WebhookResponse } from "../../../modules/app/webhook-response"; -import { PROVIDER_ORDER_ID_KEY } from "../../../modules/avatax/maps/avatax-order-fulfilled-map"; +import { PROVIDER_ORDER_ID_KEY } from "../../../modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer"; export const config = { api: { @@ -74,23 +74,17 @@ export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => { try { const appMetadata = payload.recipient?.privateMetadata ?? []; const channelSlug = payload.order?.channel.slug; - const activeTaxProvider = getActiveTaxProvider(channelSlug, appMetadata); + const taxProvider = getActiveTaxProvider(channelSlug, appMetadata); - if (!activeTaxProvider.ok) { - logger.info("Returning no data"); - return webhookResponse.failure(activeTaxProvider.error); - } - - logger.info({ activeTaxProvider }, "Fetched activeTaxProvider"); - const taxProvider = activeTaxProvider.data; + logger.info({ taxProvider }, "Fetched taxProvider"); // todo: figure out what fields are needed and add validation if (!payload.order) { - return webhookResponse.failure("Insufficient order data"); + return webhookResponse.error(new Error("Insufficient order data")); } if (payload.order.status === OrderStatus.Fulfilled) { - return webhookResponse.failure("Skipping fulfilled order to prevent duplication"); + return webhookResponse.error(new Error("Skipping fulfilled order to prevent duplication")); } const createdOrder = await taxProvider.createOrder(payload.order); @@ -104,6 +98,6 @@ export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => { return webhookResponse.success(); } catch (error) { logger.error({ error }); - return webhookResponse.failure("Error while creating order in tax provider"); + return webhookResponse.error(new Error("Error while creating order in tax provider")); } }); diff --git a/apps/taxes/src/pages/api/webhooks/order-fulfilled.ts b/apps/taxes/src/pages/api/webhooks/order-fulfilled.ts index 8bad013..b4576a9 100644 --- a/apps/taxes/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/taxes/src/pages/api/webhooks/order-fulfilled.ts @@ -36,19 +36,13 @@ export default orderFulfilledAsyncWebhook.createHandler(async (req, res, ctx) => try { const appMetadata = payload.recipient?.privateMetadata ?? []; const channelSlug = payload.order?.channel.slug; - const activeTaxProvider = getActiveTaxProvider(channelSlug, appMetadata); + const taxProvider = getActiveTaxProvider(channelSlug, appMetadata); - if (!activeTaxProvider.ok) { - logger.info("Returning no data"); - return webhookResponse.failure(activeTaxProvider.error); - } - - logger.info({ activeTaxProvider }, "Fetched activeTaxProvider"); - const taxProvider = activeTaxProvider.data; + logger.info({ taxProvider }, "Fetched taxProvider"); // todo: figure out what fields are needed and add validation if (!payload.order) { - return webhookResponse.failure("Insufficient order data"); + return webhookResponse.error(new Error("Insufficient order data")); } const fulfilledOrder = await taxProvider.fulfillOrder(payload.order); @@ -56,6 +50,6 @@ export default orderFulfilledAsyncWebhook.createHandler(async (req, res, ctx) => return webhookResponse.success(); } catch (error) { - return webhookResponse.failure("Error while fulfilling tax provider order"); + return webhookResponse.error(new Error("Error while fulfilling tax provider order")); } });