Skip to content

Commit

Permalink
feat: 🚧 add work in progress refunds
Browse files Browse the repository at this point in the history
  • Loading branch information
peelar committed Aug 28, 2023
1 parent 542bdca commit 5f82d27
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 9 deletions.
5 changes: 5 additions & 0 deletions apps/taxes/graphql/fragments/AvataxOrderMetadata.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fragment AvataxOrderMetadata on Order {
avataxEntityCode: metafield(key: "avataxEntityCode")
avataxTaxCalculationDate: metafield(key: "avataxTaxCalculationDate")
avataxDocumentCode: metafield(key: "avataxDocumentCode")
}
4 changes: 4 additions & 0 deletions apps/taxes/graphql/fragments/Money.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fragment Money on Money {
currency
amount
}
31 changes: 31 additions & 0 deletions apps/taxes/graphql/subscriptions/OrderRefunded.graphql
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions apps/taxes/src/modules/avatax/avatax-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export type VoidTransactionArgs = {
companyCode: string;
};

export type RefundTransactionParams = Parameters<Avatax["refundTransaction"]>[0];

export class AvataxClient {
private client: Avatax;

Expand Down Expand Up @@ -112,4 +114,8 @@ export class AvataxClient {
filter: `code eq ${useCode}`,
});
}

async refundTransaction(params: RefundTransactionParams) {
return this.client.refundTransaction(params);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}
Expand Down
11 changes: 5 additions & 6 deletions apps/taxes/src/modules/avatax/avatax-webhook.service.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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}`,
);
}
}
Original file line number Diff line number Diff line change
@@ -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",
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
2 changes: 1 addition & 1 deletion apps/taxes/src/pages/api/webhooks/order-refunded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});

0 comments on commit 5f82d27

Please sign in to comment.