feat: 🚧 add work in progress refunds

This commit is contained in:
Adrian Pilarczyk 2023-08-28 08:42:52 +02:00
parent 542bdcaa84
commit 5f82d274fd
11 changed files with 212 additions and 9 deletions

View file

@ -0,0 +1,5 @@
fragment AvataxOrderMetadata on Order {
avataxEntityCode: metafield(key: "avataxEntityCode")
avataxTaxCalculationDate: metafield(key: "avataxTaxCalculationDate")
avataxDocumentCode: metafield(key: "avataxDocumentCode")
}

View file

@ -0,0 +1,4 @@
fragment Money on Money {
currency
amount
}

View file

@ -1,10 +1,41 @@
fragment Payment on Payment {
transactions {
kind
amount {
...Money
}
}
}
fragment OrderRefundedSubscription on Order {
id
avataxId: metafield(key: "avataxId")
...AvataxOrderMetadata
channel {
id
slug
}
lines {
productSku
}
totalRefunded {
...Money
}
totalGrantedRefund {
...Money
}
totalRefundPending {
...Money
}
grantedRefunds {
amount {
...Money
}
reason
}
payments {
...Payment
}
}
fragment OrderRefundedEventSubscription on Event {

View file

@ -55,6 +55,8 @@ export type VoidTransactionArgs = {
companyCode: string;
};
export type RefundTransactionParams = Parameters<Avatax["refundTransaction"]>[0];
export class AvataxClient {
private client: Avatax;
@ -112,4 +114,8 @@ export class AvataxClient {
filter: `code eq ${useCode}`,
});
}
async refundTransaction(params: RefundTransactionParams) {
return this.client.refundTransaction(params);
}
}

View file

@ -4,7 +4,7 @@ export class AvataxDocumentCodeResolver {
orderId,
}: {
avataxDocumentCode: string | null | undefined;
orderId: string;
orderId: string | undefined;
}): string {
/*
* The value for "code" can be provided in the metadata.
@ -14,9 +14,12 @@ export class AvataxDocumentCodeResolver {
const code = avataxDocumentCode ?? orderId;
if (!code) {
throw new Error("Order id or document code must be provided");
}
/*
* The requirement from AvaTax API is that document code is a string that must be between 1 and 20 characters long.
* // todo: document that its sliced
*/
return code.slice(0, 20);
}

View file

@ -1,8 +1,5 @@
import { AuthData } from "@saleor/app-sdk/APL";
import {
OrderConfirmedSubscriptionFragment,
OrderRefundedSubscriptionFragment,
} from "../../../generated/graphql";
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
import { Logger, createLogger } from "../../lib/logger";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
@ -13,6 +10,7 @@ import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
import { AvataxOrderRefundedAdapter } from "./order-refunded/avatax-order-refunded-adapter";
export class AvataxWebhookService implements ProviderWebhookService {
config = defaultAvataxConfig;
@ -28,7 +26,6 @@ export class AvataxWebhookService implements ProviderWebhookService {
});
const avataxClient = new AvataxClient(config);
refundOrder: (payload: OrderRefundedSubscriptionFragment) => Promise<void>;
this.config = config;
this.client = avataxClient;
}
@ -56,6 +53,8 @@ export class AvataxWebhookService implements ProviderWebhookService {
}
async refundOrder(payload: OrderRefundedPayload) {
// todo: implement
const adapter = new AvataxOrderRefundedAdapter(this.config);
return adapter.send(payload);
}
}

View file

@ -0,0 +1,89 @@
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<RefundTransactionParams, "transactionCode" | "companyCode"> = {
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(),
},
};
}
}
export class AvataxOrderRefundedAdapter implements WebhookAdapter<OrderRefundedPayload, void> {
private logger: Logger;
constructor(private readonly config: AvataxConfig) {
this.logger = createLogger({ name: "AvataxOrderRefundedAdapter" });
}
async send(payload: OrderRefundedPayload) {
this.logger.debug(
{ payload },
"Transforming the Saleor payload for refunding order with AvaTax...",
);
if (!this.config.isAutocommit) {
throw new Error(
"Unable to refund transaction. AvaTax can only refund commited transactions.",
);
}
const client = new AvataxClient(this.config);
const payloadTransformer = new AvataxOrderRefundedPayloadTransformer();
const target = payloadTransformer.transform(payload, this.config);
const response = await client.refundTransaction(target);
this.logger.debug(
{ response },
`Succesfully refunded the transaction of id: ${target.transactionCode}`,
);
}
}

View file

@ -0,0 +1,53 @@
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",
},
]);
});
});

View file

@ -0,0 +1,11 @@
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);
}
}

View file

@ -48,9 +48,11 @@ export class TaxJarWebhookService implements ProviderWebhookService {
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
// todo: implement
this.logger.debug("cancelOrder not implement for TaxJar");
}
async refundOrder(payload: OrderRefundedPayload) {
// todo: implement
this.logger.debug("refundOrder not implement for TaxJar");
}
}

View file

@ -51,6 +51,6 @@ export default orderRefundedAsyncWebhook.createHandler(async (req, res, ctx) =>
return webhookResponse.success();
} catch (error) {
return webhookResponse.error(new Error("Error while refunding tax provider order"));
return webhookResponse.error(error);
}
});