feat: 🚧 add work in progress refunds
This commit is contained in:
parent
542bdcaa84
commit
5f82d274fd
11 changed files with 212 additions and 9 deletions
5
apps/taxes/graphql/fragments/AvataxOrderMetadata.graphql
Normal file
5
apps/taxes/graphql/fragments/AvataxOrderMetadata.graphql
Normal file
|
@ -0,0 +1,5 @@
|
|||
fragment AvataxOrderMetadata on Order {
|
||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||
avataxTaxCalculationDate: metafield(key: "avataxTaxCalculationDate")
|
||||
avataxDocumentCode: metafield(key: "avataxDocumentCode")
|
||||
}
|
4
apps/taxes/graphql/fragments/Money.graphql
Normal file
4
apps/taxes/graphql/fragments/Money.graphql
Normal file
|
@ -0,0 +1,4 @@
|
|||
fragment Money on Money {
|
||||
currency
|
||||
amount
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue