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 {
|
fragment OrderRefundedSubscription on Order {
|
||||||
id
|
id
|
||||||
avataxId: metafield(key: "avataxId")
|
avataxId: metafield(key: "avataxId")
|
||||||
|
...AvataxOrderMetadata
|
||||||
channel {
|
channel {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
|
lines {
|
||||||
|
productSku
|
||||||
|
}
|
||||||
|
totalRefunded {
|
||||||
|
...Money
|
||||||
|
}
|
||||||
|
totalGrantedRefund {
|
||||||
|
...Money
|
||||||
|
}
|
||||||
|
totalRefundPending {
|
||||||
|
...Money
|
||||||
|
}
|
||||||
|
grantedRefunds {
|
||||||
|
amount {
|
||||||
|
...Money
|
||||||
|
}
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
payments {
|
||||||
|
...Payment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment OrderRefundedEventSubscription on Event {
|
fragment OrderRefundedEventSubscription on Event {
|
||||||
|
|
|
@ -55,6 +55,8 @@ export type VoidTransactionArgs = {
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RefundTransactionParams = Parameters<Avatax["refundTransaction"]>[0];
|
||||||
|
|
||||||
export class AvataxClient {
|
export class AvataxClient {
|
||||||
private client: Avatax;
|
private client: Avatax;
|
||||||
|
|
||||||
|
@ -112,4 +114,8 @@ export class AvataxClient {
|
||||||
filter: `code eq ${useCode}`,
|
filter: `code eq ${useCode}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refundTransaction(params: RefundTransactionParams) {
|
||||||
|
return this.client.refundTransaction(params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ export class AvataxDocumentCodeResolver {
|
||||||
orderId,
|
orderId,
|
||||||
}: {
|
}: {
|
||||||
avataxDocumentCode: string | null | undefined;
|
avataxDocumentCode: string | null | undefined;
|
||||||
orderId: string;
|
orderId: string | undefined;
|
||||||
}): string {
|
}): string {
|
||||||
/*
|
/*
|
||||||
* The value for "code" can be provided in the metadata.
|
* The value for "code" can be provided in the metadata.
|
||||||
|
@ -14,9 +14,12 @@ export class AvataxDocumentCodeResolver {
|
||||||
|
|
||||||
const code = avataxDocumentCode ?? orderId;
|
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.
|
* 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);
|
return code.slice(0, 20);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||||
OrderConfirmedSubscriptionFragment,
|
|
||||||
OrderRefundedSubscriptionFragment,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
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 { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
||||||
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
||||||
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
||||||
|
import { AvataxOrderRefundedAdapter } from "./order-refunded/avatax-order-refunded-adapter";
|
||||||
|
|
||||||
export class AvataxWebhookService implements ProviderWebhookService {
|
export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
config = defaultAvataxConfig;
|
config = defaultAvataxConfig;
|
||||||
|
@ -28,7 +26,6 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
});
|
});
|
||||||
const avataxClient = new AvataxClient(config);
|
const avataxClient = new AvataxClient(config);
|
||||||
|
|
||||||
refundOrder: (payload: OrderRefundedSubscriptionFragment) => Promise<void>;
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.client = avataxClient;
|
this.client = avataxClient;
|
||||||
}
|
}
|
||||||
|
@ -56,6 +53,8 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async refundOrder(payload: OrderRefundedPayload) {
|
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) {
|
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
|
||||||
// todo: implement
|
// todo: implement
|
||||||
|
this.logger.debug("cancelOrder not implement for TaxJar");
|
||||||
}
|
}
|
||||||
|
|
||||||
async refundOrder(payload: OrderRefundedPayload) {
|
async refundOrder(payload: OrderRefundedPayload) {
|
||||||
// todo: implement
|
// 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();
|
return webhookResponse.success();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return webhookResponse.error(new Error("Error while refunding tax provider order"));
|
return webhookResponse.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue