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
|
||||
---
|
||||
|
||||
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 { 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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
|
||||
return this.client.fulfillOrder(payload);
|
||||
}
|
||||
|
||||
async cancelOrder(payload: OrderCancelledPayload) {
|
||||
this.client.cancelOrder(payload);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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