refactor: 🔥 order_fulfilled webhook

This commit is contained in:
Adrian Pilarczyk 2023-08-03 10:50:50 +02:00
parent 09c3be9ad2
commit 2b073e7113
10 changed files with 1 additions and 318 deletions

View file

@ -2,5 +2,4 @@
"saleor-app-taxes": minor
---
Changed the order_created to order_confirmed webhook event. Now, the provider transactions will be created based on the order confirmation (either automatic or manual).
Changed the order_created to order_confirmed webhook event. Now, the provider transactions will be created based on the order confirmation (either automatic or manual). Also, removed the order_fulfilled webhook event handler. The value of the "commit" field is now set only based on the "isAutocommit" setting in the provider configuration.

View file

@ -12,7 +12,6 @@ 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 { AvataxOrderFulfilledAdapter } from "./order-fulfilled/avatax-order-fulfilled-adapter";
export class AvataxWebhookService implements ProviderWebhookService {
config = defaultAvataxConfig;
@ -45,14 +44,6 @@ export class AvataxWebhookService implements ProviderWebhookService {
return response;
}
async fulfillOrder(order: OrderFulfilledSubscriptionFragment) {
const adapter = new AvataxOrderFulfilledAdapter(this.config);
const response = await adapter.send({ order });
return response;
}
async cancelOrder(payload: OrderCancelledPayload) {
const adapter = new AvataxOrderCancelledAdapter(this.config);

View file

@ -1,44 +0,0 @@
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
import { Logger, createLogger } from "../../../lib/logger";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient, CommitTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer";
export type AvataxOrderFulfilledPayload = {
order: OrderFulfilledSubscriptionFragment;
};
export type AvataxOrderFulfilledTarget = CommitTransactionArgs;
export type AvataxOrderFulfilledResponse = { ok: true };
export class AvataxOrderFulfilledAdapter
implements WebhookAdapter<AvataxOrderFulfilledPayload, AvataxOrderFulfilledResponse>
{
private logger: Logger;
constructor(private readonly config: AvataxConfig) {
this.logger = createLogger({ name: "AvataxOrderFulfilledAdapter" });
}
async send(payload: AvataxOrderFulfilledPayload): Promise<AvataxOrderFulfilledResponse> {
this.logger.debug("Transforming the Saleor payload for commiting transaction with Avatax...");
const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer(this.config);
const target = payloadTransformer.transform({ ...payload });
this.logger.debug("Calling Avatax commitTransaction with transformed payload...");
const client = new AvataxClient(this.config);
const response = await client.commitTransaction(target);
this.logger.debug("Avatax commitTransaction succesfully responded");
const responseTransformer = new AvataxOrderFulfilledResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed Avatax commitTransaction response");
return transformedResponse;
}
}

View file

@ -1,163 +0,0 @@
import { DocumentType } from "avatax/lib/enums/DocumentType";
import { describe, expect, it } from "vitest";
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
// todo: add AvataxOrderFulfilledMockGenerator
const MOCK_AVATAX_CONFIG: AvataxConfig = {
companyCode: "DEFAULT",
isDocumentRecordingEnabled: true,
isAutocommit: false,
isSandbox: true,
name: "Avatax-1",
shippingTaxCode: "FR000000",
address: {
country: "US",
zip: "10118",
state: "NY",
city: "New York",
street: "350 5th Avenue",
},
credentials: {
password: "password",
username: "username",
},
};
type OrderFulfilled = OrderFulfilledSubscriptionFragment;
const ORDER_FULFILLED_MOCK: OrderFulfilled = {
id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5",
created: "2023-04-11T11:03:09.304109+00:00",
avataxId: "transaction-code",
channel: {
id: "Q2hhbm5lbDoy",
slug: "channel-pln",
},
shippingAddress: {
streetAddress1: "123 Palm Grove Ln",
streetAddress2: "",
city: "LOS ANGELES",
countryArea: "CA",
postalCode: "90002",
country: {
code: "US",
},
},
billingAddress: {
streetAddress1: "123 Palm Grove Ln",
streetAddress2: "",
city: "LOS ANGELES",
countryArea: "CA",
postalCode: "90002",
country: {
code: "US",
},
},
total: {
net: {
amount: 183.33,
},
tax: {
amount: 12.83,
},
},
shippingPrice: {
net: {
amount: 48.33,
},
},
lines: [
{
productSku: "328223581",
productName: "Monospace Tee",
quantity: 1,
unitPrice: {
net: {
amount: 90,
},
},
totalPrice: {
net: {
amount: 90,
},
tax: {
amount: 8.55,
},
},
},
{
productSku: "328223580",
productName: "Polyspace Tee",
quantity: 1,
unitPrice: {
net: {
amount: 45,
},
},
totalPrice: {
net: {
amount: 45,
},
tax: {
amount: 4.28,
},
},
},
],
};
const MOCKED_ORDER_FULFILLED_PAYLOAD: {
order: OrderFulfilledSubscriptionFragment;
} = {
order: ORDER_FULFILLED_MOCK,
};
describe("AvataxOrderFulfilledPayloadTransformer", () => {
it("throws error when no avataxId", () => {
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
expect(() =>
transformer.transform({
...MOCKED_ORDER_FULFILLED_PAYLOAD,
order: {
...MOCKED_ORDER_FULFILLED_PAYLOAD.order,
avataxId: null,
},
})
).toThrow();
});
it("returns document type of SalesOrder when isDocumentRecordingEnabled is false", () => {
const transformer = new AvataxOrderFulfilledPayloadTransformer({
...MOCK_AVATAX_CONFIG,
isDocumentRecordingEnabled: false,
});
const payload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD);
expect(payload.documentType).toBe(DocumentType.SalesOrder);
}),
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", () => {
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
const payload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD);
expect(payload.documentType).toBe(DocumentType.SalesInvoice);
}),
it("returns transformed payload", () => {
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
const mappedPayload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD);
expect(mappedPayload).toEqual({
transactionCode: "transaction-code",
companyCode: "DEFAULT",
documentType: DocumentType.SalesInvoice,
model: {
commit: true,
},
});
});
});

View file

@ -1,30 +0,0 @@
import { DocumentType } from "avatax/lib/enums/DocumentType";
import { z } from "zod";
import { AvataxConfig } from "../avatax-connection-schema";
import {
AvataxOrderFulfilledPayload,
AvataxOrderFulfilledTarget,
} from "./avatax-order-fulfilled-adapter";
export class AvataxOrderFulfilledPayloadTransformer {
constructor(private readonly config: AvataxConfig) {}
private matchDocumentType(config: AvataxConfig): DocumentType {
if (!config.isDocumentRecordingEnabled) {
return DocumentType.SalesOrder;
}
return DocumentType.SalesInvoice;
}
transform({ order }: AvataxOrderFulfilledPayload): AvataxOrderFulfilledTarget {
const transactionCode = z.string().min(1).parse(order.avataxId);
return {
transactionCode,
companyCode: this.config.companyCode,
documentType: this.matchDocumentType(this.config),
model: {
commit: true,
},
};
}
}

View file

@ -1,8 +0,0 @@
import { TransactionModel } from "avatax/lib/models/TransactionModel";
import { AvataxOrderFulfilledResponse } from "./avatax-order-fulfilled-adapter";
export class AvataxOrderFulfilledResponseTransformer {
transform(response: TransactionModel): AvataxOrderFulfilledResponse {
return { ok: true };
}
}

View file

@ -54,10 +54,6 @@ class ActiveTaxProviderService implements ProviderWebhookService {
return this.client.confirmOrder(order);
}
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
return this.client.fulfillOrder(payload);
}
async cancelOrder(payload: OrderCancelledPayload) {
this.client.cancelOrder(payload);
}

View file

@ -13,6 +13,5 @@ export type CreateOrderResponse = { id: string };
export interface ProviderWebhookService {
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
fulfillOrder: (payload: OrderFulfilledSubscriptionFragment) => Promise<{ ok: boolean }>;
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
}

View file

@ -5,7 +5,6 @@ import packageJson from "../../../package.json";
import { checkoutCalculateTaxesSyncWebhook } from "./webhooks/checkout-calculate-taxes";
import { orderCalculateTaxesSyncWebhook } from "./webhooks/order-calculate-taxes";
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
import { orderFulfilledAsyncWebhook } from "./webhooks/order-fulfilled";
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
@ -37,7 +36,6 @@ export default createManifestHandler({
orderCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
orderFulfilledAsyncWebhook.getWebhookManifest(apiBaseURL),
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
],
};

View file

@ -1,55 +0,0 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
OrderFulfilledEventSubscriptionFragment,
UntypedOrderFulfilledSubscriptionDocument,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { WebhookResponse } from "../../../modules/app/webhook-response";
export const config = {
api: {
bodyParser: false,
},
};
type OrderFulfilledPayload = Extract<
OrderFulfilledEventSubscriptionFragment,
{ __typename: "OrderFulfilled" }
>;
export const orderFulfilledAsyncWebhook = new SaleorAsyncWebhook<OrderFulfilledPayload>({
name: "OrderFulfilled",
apl: saleorApp.apl,
event: "ORDER_FULFILLED",
query: UntypedOrderFulfilledSubscriptionDocument,
webhookPath: "/api/webhooks/order-fulfilled",
});
export default orderFulfilledAsyncWebhook.createHandler(async (req, res, ctx) => {
const logger = createLogger({ event: ctx.event });
const { payload } = ctx;
const webhookResponse = new WebhookResponse(res);
logger.info("Handler called with payload");
try {
const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.order?.channel.slug;
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
// todo: figure out what fields are needed and add validation
if (!payload.order) {
return webhookResponse.error(new Error("Insufficient order data"));
}
logger.info("Fulfilling order...");
await taxProvider.fulfillOrder(payload.order);
logger.info("Order fulfilled");
return webhookResponse.success();
} catch (error) {
return webhookResponse.error(error);
}
});