refactor: 🔥 order_fulfilled webhook
This commit is contained in:
parent
09c3be9ad2
commit
2b073e7113
10 changed files with 1 additions and 318 deletions
|
@ -2,5 +2,4 @@
|
||||||
"saleor-app-taxes": minor
|
"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.
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ 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 { AvataxOrderFulfilledAdapter } from "./order-fulfilled/avatax-order-fulfilled-adapter";
|
|
||||||
|
|
||||||
export class AvataxWebhookService implements ProviderWebhookService {
|
export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
config = defaultAvataxConfig;
|
config = defaultAvataxConfig;
|
||||||
|
@ -45,14 +44,6 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfillOrder(order: OrderFulfilledSubscriptionFragment) {
|
|
||||||
const adapter = new AvataxOrderFulfilledAdapter(this.config);
|
|
||||||
|
|
||||||
const response = await adapter.send({ order });
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledPayload) {
|
async cancelOrder(payload: OrderCancelledPayload) {
|
||||||
const adapter = new AvataxOrderCancelledAdapter(this.config);
|
const adapter = new AvataxOrderCancelledAdapter(this.config);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -54,10 +54,6 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
||||||
return this.client.confirmOrder(order);
|
return this.client.confirmOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
|
|
||||||
return this.client.fulfillOrder(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledPayload) {
|
async cancelOrder(payload: OrderCancelledPayload) {
|
||||||
this.client.cancelOrder(payload);
|
this.client.cancelOrder(payload);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,5 @@ export type CreateOrderResponse = { id: string };
|
||||||
export interface ProviderWebhookService {
|
export interface ProviderWebhookService {
|
||||||
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
||||||
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||||
fulfillOrder: (payload: OrderFulfilledSubscriptionFragment) => Promise<{ ok: boolean }>;
|
|
||||||
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import packageJson from "../../../package.json";
|
||||||
import { checkoutCalculateTaxesSyncWebhook } from "./webhooks/checkout-calculate-taxes";
|
import { checkoutCalculateTaxesSyncWebhook } from "./webhooks/checkout-calculate-taxes";
|
||||||
import { orderCalculateTaxesSyncWebhook } from "./webhooks/order-calculate-taxes";
|
import { orderCalculateTaxesSyncWebhook } from "./webhooks/order-calculate-taxes";
|
||||||
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
|
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
|
||||||
import { orderFulfilledAsyncWebhook } from "./webhooks/order-fulfilled";
|
|
||||||
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
|
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
|
||||||
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
|
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
|
||||||
|
|
||||||
|
@ -37,7 +36,6 @@ export default createManifestHandler({
|
||||||
orderCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
orderCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderFulfilledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
|
||||||
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
Loading…
Reference in a new issue