diff --git a/apps/taxes/graphql/fragments/OrderLine.graphql b/apps/taxes/graphql/fragments/OrderLine.graphql new file mode 100644 index 0000000..6718615 --- /dev/null +++ b/apps/taxes/graphql/fragments/OrderLine.graphql @@ -0,0 +1,22 @@ +fragment OrderLine on OrderLine { + id + productSku + productName + quantity + taxClass { + id + } + unitPrice { + net { + amount + } + } + totalPrice { + net { + amount + } + tax { + amount + } + } +} diff --git a/apps/taxes/graphql/subscriptions/OrderConfirmed.graphql b/apps/taxes/graphql/subscriptions/OrderConfirmed.graphql index 889ff4a..0f0a01c 100644 --- a/apps/taxes/graphql/subscriptions/OrderConfirmed.graphql +++ b/apps/taxes/graphql/subscriptions/OrderConfirmed.graphql @@ -1,25 +1,3 @@ -fragment OrderLine on OrderLine { - productSku - productName - quantity - taxClass { - id - } - unitPrice { - net { - amount - } - } - totalPrice { - net { - amount - } - tax { - amount - } - } -} - fragment OrderConfirmedSubscription on Order { id number diff --git a/apps/taxes/graphql/subscriptions/OrderRefunded.graphql b/apps/taxes/graphql/subscriptions/OrderRefunded.graphql index 08132fe..65277d2 100644 --- a/apps/taxes/graphql/subscriptions/OrderRefunded.graphql +++ b/apps/taxes/graphql/subscriptions/OrderRefunded.graphql @@ -1,40 +1,23 @@ -fragment Payment on Payment { - transactions { - kind - amount { - ...Money - } - } -} - fragment OrderRefundedSubscription on Order { id avataxId: metafield(key: "avataxId") ...AvataxOrderMetadata + user { + id + email + } channel { id slug } - lines { - productSku - } - totalRefunded { - ...Money - } - totalGrantedRefund { - ...Money - } - totalRefundPending { - ...Money - } - grantedRefunds { - amount { + transactions { + id + refundedAmount { ...Money } - reason } - payments { - ...Payment + shippingAddress { + ...Address } } diff --git a/apps/taxes/src/mocks.ts b/apps/taxes/src/mocks.ts index 19e56c2..9dda98e 100644 --- a/apps/taxes/src/mocks.ts +++ b/apps/taxes/src/mocks.ts @@ -53,6 +53,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = { }, lines: [ { + id: "T3JkZXJMaW5lOjE=", productSku: "328223580", productName: "Monospace Tee", quantity: 3, @@ -71,6 +72,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = { }, }, { + id: "T3JkZXJMaW5lOjI=", productSku: "328223581", productName: "Monospace Tee", quantity: 1, @@ -89,6 +91,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = { }, }, { + id: "T3JkZXJMaW5lOjM=", productSku: "118223581", productName: "Paul's Balance 420", quantity: 2, diff --git a/apps/taxes/src/modules/avatax/avatax-client.ts b/apps/taxes/src/modules/avatax/avatax-client.ts index d681809..858974e 100644 --- a/apps/taxes/src/modules/avatax/avatax-client.ts +++ b/apps/taxes/src/modules/avatax/avatax-client.ts @@ -55,7 +55,10 @@ export type VoidTransactionArgs = { companyCode: string; }; -export type RefundTransactionParams = Parameters[0]; +export type RefundTransactionParams = Pick< + CreateTransactionModel, + "customerCode" | "lines" | "date" | "addresses" | "code" | "companyCode" +>; export class AvataxClient { private client: Avatax; @@ -116,6 +119,15 @@ export class AvataxClient { } async refundTransaction(params: RefundTransactionParams) { - return this.client.refundTransaction(params); + // https://developer.avalara.com/erp-integration-guide/refunds-badge/refunds-with-create-transactions/ + return this.client.createOrAdjustTransaction({ + model: { + createTransactionModel: { + type: DocumentType.ReturnInvoice, + commit: true, + ...params, + }, + }, + }); } } diff --git a/apps/taxes/src/modules/avatax/order-confirmed/avatax-address-resolver.ts b/apps/taxes/src/modules/avatax/order-confirmed/avatax-address-resolver.ts index f9a04ad..17856d8 100644 --- a/apps/taxes/src/modules/avatax/order-confirmed/avatax-address-resolver.ts +++ b/apps/taxes/src/modules/avatax/order-confirmed/avatax-address-resolver.ts @@ -1,7 +1,8 @@ +import { AddressesModel } from "avatax/lib/models/AddressesModel"; import { AddressFragment } from "../../../../generated/graphql"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; import { avataxAddressFactory } from "../address-factory"; import { AvataxConfig } from "../avatax-connection-schema"; -import { CreateTransactionModel } from "avatax/lib/models/CreateTransactionModel"; export class AvataxAddressResolver { resolve({ @@ -9,11 +10,11 @@ export class AvataxAddressResolver { to, }: { from: AvataxConfig["address"]; - to: AddressFragment; - }): CreateTransactionModel["addresses"] { + to: AddressFragment | undefined | null; + }): AddressesModel { return { shipFrom: avataxAddressFactory.fromChannelAddress(from), - shipTo: avataxAddressFactory.fromSaleorAddress(to), + shipTo: avataxAddressFactory.fromSaleorAddress(taxProviderUtils.resolveOptionalOrThrow(to)), }; } } diff --git a/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-adapter.ts b/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-adapter.ts index 0598799..b976e04 100644 --- a/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-adapter.ts +++ b/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-adapter.ts @@ -1,60 +1,9 @@ -import { RefundType } from "avatax/lib/enums/RefundType"; -import { z } from "zod"; import { Logger, createLogger } from "../../../lib/logger"; import { OrderRefundedPayload } from "../../../pages/api/webhooks/order-refunded"; import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; -import { AvataxClient, RefundTransactionParams } from "../avatax-client"; -import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema"; -import { taxProviderUtils } from "../../taxes/tax-provider-utils"; - -class AvataxOrderRefundedPayloadTransformer { - private logger: Logger; - - constructor() { - this.logger = createLogger({ name: "AvataxOrderRefundedPayloadTransformer" }); - } - - transform(payload: OrderRefundedPayload, avataxConfig: AvataxConfig): RefundTransactionParams { - this.logger.debug( - { payload }, - "Transforming the Saleor payload for refunding order with AvaTax...", - ); - - const isFull = true; - - const transactionCode = z - .string() - .min(1, "Unable to refund transaction. Avatax id not found in order metadata") - .parse(payload.order?.avataxId); - - const baseParams: Pick = { - transactionCode, - companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode, - }; - - if (!isFull) { - return { - ...baseParams, - model: { - refundType: RefundType.Partial, - refundDate: new Date(), - refundLines: payload.order?.lines?.map((line) => - // todo: replace with some other code - taxProviderUtils.resolveStringOrThrow(line.productSku), - ), - }, - }; - } - - return { - ...baseParams, - model: { - refundType: RefundType.Full, - refundDate: new Date(), - }, - }; - } -} +import { AvataxClient } from "../avatax-client"; +import { AvataxConfig } from "../avatax-connection-schema"; +import { AvataxOrderRefundedPayloadTransformer } from "./avatax-order-refunded-payload-transformer"; export class AvataxOrderRefundedAdapter implements WebhookAdapter { private logger: Logger; @@ -79,11 +28,15 @@ export class AvataxOrderRefundedAdapter implements WebhookAdapter t.refundedAmount.amount > 0) ?? []; + + if (!refundTransactions.length) { + throw new Error("Cannot refund order without any refund transactions"); + } + + return refundTransactions.map((t) => ({ + amount: -t.refundedAmount.amount, + taxIncluded: true, + })); + } +} diff --git a/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-payload-transformer.ts b/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-payload-transformer.ts new file mode 100644 index 0000000..c84563f --- /dev/null +++ b/apps/taxes/src/modules/avatax/order-refunded/avatax-order-refunded-payload-transformer.ts @@ -0,0 +1,47 @@ +import { Logger, createLogger } from "../../../lib/logger"; +import { OrderRefundedPayload } from "../../../pages/api/webhooks/order-refunded"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; +import { RefundTransactionParams } from "../avatax-client"; +import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema"; +import { AvataxDocumentCodeResolver } from "../avatax-document-code-resolver"; +import { AvataxAddressResolver } from "../order-confirmed/avatax-address-resolver"; +import { AvataxOrderRefundedLinesTransformer } from "./avatax-order-refunded-lines-transformer"; + +export class AvataxOrderRefundedPayloadTransformer { + private logger: Logger; + + constructor() { + this.logger = createLogger({ name: "AvataxOrderRefundedPayloadTransformer" }); + } + + transform(payload: OrderRefundedPayload, avataxConfig: AvataxConfig): RefundTransactionParams { + this.logger.debug( + { payload }, + "Transforming the Saleor payload for refunding order with AvaTax...", + ); + + const addressResolver = new AvataxAddressResolver(); + const linesTransformer = new AvataxOrderRefundedLinesTransformer(); + const documentCodeResolver = new AvataxDocumentCodeResolver(); + + const addresses = addressResolver.resolve({ + from: avataxConfig.address, + to: payload.order?.shippingAddress, + }); + const lines = linesTransformer.transform(payload); + const customerCode = taxProviderUtils.resolveStringOrThrow(payload.order?.user?.id); + const code = documentCodeResolver.resolve({ + avataxDocumentCode: payload.order?.avataxDocumentCode, + orderId: payload.order?.id, + }); + + return { + code, + lines, + customerCode, + addresses, + date: new Date(), + companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode, + }; + } +} diff --git a/apps/taxes/src/modules/avatax/order-refunded/avatax-refunds-resolver.test.ts b/apps/taxes/src/modules/avatax/order-refunded/avatax-refunds-resolver.test.ts deleted file mode 100644 index 10d0b46..0000000 --- a/apps/taxes/src/modules/avatax/order-refunded/avatax-refunds-resolver.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { PaymentFragment, TransactionKind } from "../../../../generated/graphql"; -import { AvataxRefundsResolver } from "./avatax-refunds-resolver"; -import { expect, describe, it } from "vitest"; - -describe("AvataxRefundsResolver", () => { - it("returns transaction amounts for refunds", () => { - const resolver = new AvataxRefundsResolver(); - const mockPayments: PaymentFragment[] = [ - { - transactions: [ - { - kind: TransactionKind.Refund, - amount: { - amount: 20.0, - currency: "USD", - }, - }, - { - kind: TransactionKind.Capture, - amount: { - amount: 20.0, - currency: "USD", - }, - }, - ], - }, - { - transactions: [ - { - kind: TransactionKind.Refund, - amount: { - amount: 35.0, - currency: "USD", - }, - }, - ], - }, - ]; - - const refunds = resolver.resolve(mockPayments); - - expect(refunds).toEqual([ - { - amount: 20.0, - currency: "USD", - }, - { - amount: 35.0, - currency: "USD", - }, - ]); - }); -}); diff --git a/apps/taxes/src/modules/avatax/order-refunded/avatax-refunds-resolver.ts b/apps/taxes/src/modules/avatax/order-refunded/avatax-refunds-resolver.ts deleted file mode 100644 index c8f17dc..0000000 --- a/apps/taxes/src/modules/avatax/order-refunded/avatax-refunds-resolver.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PaymentFragment, TransactionKind } from "../../../../generated/graphql"; - -export class AvataxRefundsResolver { - resolve(payments: PaymentFragment[]) { - return payments - .flatMap( - (payment) => payment.transactions?.filter((t) => t.kind === TransactionKind.Refund) ?? [], - ) - .map((t) => t.amount); - } -} diff --git a/apps/taxes/src/modules/taxjar/address-factory.ts b/apps/taxes/src/modules/taxjar/address-factory.ts index b9ab37c..e508e99 100644 --- a/apps/taxes/src/modules/taxjar/address-factory.ts +++ b/apps/taxes/src/modules/taxjar/address-factory.ts @@ -8,7 +8,7 @@ function joinAddresses(address1: string, address2: string): string { } function mapSaleorAddressToTaxJarAddress( - address: SaleorAddress + address: SaleorAddress, ): Pick { return { to_street: joinAddresses(address.streetAddress1, address.streetAddress2), @@ -20,7 +20,7 @@ function mapSaleorAddressToTaxJarAddress( } function mapChannelAddressToTaxJarAddress( - address: TaxJarConfig["address"] + address: TaxJarConfig["address"], ): Pick { return { from_city: address.city, @@ -31,7 +31,7 @@ function mapChannelAddressToTaxJarAddress( }; } -function mapChannelAddressToAddressParams(address: TaxJarConfig["address"]): AddressParams { +function mapChannelAddresstoParams(address: TaxJarConfig["address"]): AddressParams { return { city: address.city, country: address.country, @@ -44,5 +44,5 @@ function mapChannelAddressToAddressParams(address: TaxJarConfig["address"]): Add export const taxJarAddressFactory = { fromSaleorToTax: mapSaleorAddressToTaxJarAddress, fromChannelToTax: mapChannelAddressToTaxJarAddress, - fromChannelToParams: mapChannelAddressToAddressParams, + fromChannelToParams: mapChannelAddresstoParams, }; 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 index 50a1de4..a4e0ff1 100644 --- 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 @@ -9,19 +9,19 @@ export class TaxJarCalculateTaxesPayloadTransformer { constructor(private readonly config: TaxJarConfig) {} transform(taxBase: TaxBaseFragment, matches: TaxJarTaxCodeMatches): TaxJarCalculateTaxesTarget { - const fromAddress = taxJarAddressFactory.fromChannelToTax(this.config.address); + const from = taxJarAddressFactory.fromChannelToTax(this.config.address); if (!taxBase.address) { throw new Error("Customer address is required to calculate taxes in TaxJar."); } const lineTransformer = new TaxJarCalculateTaxesPayloadLinesTransformer(); - const toAddress = taxJarAddressFactory.fromSaleorToTax(taxBase.address); + const to = taxJarAddressFactory.fromSaleorToTax(taxBase.address); const taxParams: TaxJarCalculateTaxesTarget = { params: { - ...fromAddress, - ...toAddress, + ...from, + ...to, shipping: taxBase.shippingPrice.amount, line_items: lineTransformer.transform(taxBase, matches), }, diff --git a/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-payload-lines-transformer.test.ts b/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-payload-lines-transformer.test.ts index 8c741ed..ff3f21b 100644 --- a/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-payload-lines-transformer.test.ts +++ b/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-payload-lines-transformer.test.ts @@ -8,6 +8,7 @@ const transformer = new TaxJarOrderConfirmedPayloadLinesTransformer(); const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [ { + id: "T3JkZXJMaW5lOjE=", productSku: "sku", productName: "Test product", quantity: 1, @@ -29,6 +30,7 @@ const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [ }, }, { + id: "T3JkZXJMaW5lOjF=", productSku: "sku-2", productName: "Test product 2", quantity: 2, diff --git a/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-tax-code-matcher.test.ts b/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-tax-code-matcher.test.ts index 15fca46..5fdf357 100644 --- a/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-tax-code-matcher.test.ts +++ b/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-tax-code-matcher.test.ts @@ -4,6 +4,7 @@ import { TaxJarOrderConfirmedTaxCodeMatcher } from "./taxjar-order-confirmed-tax import { describe, expect, it } from "vitest"; const mockedLine: OrderLineFragment = { + id: "T3JkZXJMaW5lOjE=", productSku: "sku", productName: "Test product", quantity: 1,