Compare commits

...

21 commits

Author SHA1 Message Date
Adrian Pilarczyk
030e373eb9 refactor: ♻️ metafield -> privateMetafield 2023-08-04 09:12:53 +02:00
Adrian Pilarczyk
b2f6226648 build: 👷 add changeset 2023-08-03 11:49:47 +02:00
Adrian Pilarczyk
25696d6a98 feat: provide document code through metadata field 2023-08-03 11:49:47 +02:00
Adrian Pilarczyk
6106e1d5fa
feat: Avatax metadata tax calculation date (#843)
* feat:  add metadata tax calculation date

* build: 👷 add changeset
2023-08-03 11:48:43 +02:00
Adrian Pilarczyk
2b073e7113 refactor: 🔥 order_fulfilled webhook 2023-08-03 10:52:19 +02:00
Adrian Pilarczyk
09c3be9ad2 build: 👷 add changeset 2023-08-03 09:33:08 +02:00
Adrian Pilarczyk
c3cc7a8b5e fix: 🐛 change voidReason 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
32a7a91294 refactor: 🚚 order_created -> order_confirmed 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
7cb991abaf fix: ⚗️ fix voidTransaction type 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
293407e9f0 fix: 🐛 use "DEFAULT" value of companyCode for commit to work 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
79a1411a55 fix: 🐛 split privateMetadata and publicMetadata 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
547e773500 refactor: ♻️ extend logging in webhook response errors 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
fb8aa45f4c build: ⬆️ upgrade avatax 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
d2a9686937 refactor: ♻️ split up webhook response 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
e4ac0e7827 refactor: avataxId instead of externalId 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
ac9acdf0c6 test: add tests for AvataxOrderCancelledPayloadTransformer 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
d458452573 feat: add order-cancelled avatax adapter 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
0013af2f6d Revert "feat: add readExternalIdFromOrderMetadata"
This reverts commit a78d9d4597672f8605cf998a9f784aebaab27de1.
2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
b167fe1b4a feat: add readExternalIdFromOrderMetadata 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
146517062a feat: 🚧 add basic boilerplate 2023-08-03 09:25:15 +02:00
Adrian Pilarczyk
1a41ca7684 refactor: ♻️ extract order-metadata-manager 2023-08-03 09:25:15 +02:00
67 changed files with 595 additions and 566 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": minor
---
Changed `externalId` order metadata field to `avataxId`. It is now only responsible for storing the id of Avatax transaction.

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": minor
---
Added support for reading document code from metadata field `avataxDocumentCode`. The default value is the order id from Saleor.

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": minor
---
Added support for reading the tax calculation date from metadata field `avataxTaxCalculationDate`. The value has to be valid UTC datetime string (e.g. "2021-08-31T13:00:00.000Z").

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": minor
---
Added order_cancelled webhook handler with an implemention for Avatax. The transactions for corresponding orders in Saleor will be voided in Avatax dashboard.

View file

@ -0,0 +1,5 @@
---
"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). 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

@ -61,13 +61,13 @@ fragment TaxBase on TaxableObject {
} }
sourceObject { sourceObject {
... on Checkout { ... on Checkout {
avataxEntityCode: metafield(key: "avataxEntityCode") avataxEntityCode: privateMetafield(key: "avataxEntityCode")
user { user {
id id
} }
} }
... on Order { ... on Order {
avataxEntityCode: metafield(key: "avataxEntityCode") avataxEntityCode: privateMetafield(key: "avataxEntityCode")
user { user {
id id
} }

View file

@ -1,4 +1,4 @@
mutation UpdateMetadata($id: ID!, $input: [MetadataInput!]!) { mutation UpdatePrivateMetadata($id: ID!, $input: [MetadataInput!]!) {
updatePrivateMetadata(id: $id, input: $input) { updatePrivateMetadata(id: $id, input: $input) {
item { item {
privateMetadata { privateMetadata {

View file

@ -0,0 +1,10 @@
mutation UpdatePublicMetadata($id: ID!, $input: [MetadataInput!]!) {
updateMetadata(id: $id, input: $input) {
item {
metadata {
key
value
}
}
}
}

View file

@ -0,0 +1,29 @@
fragment OrderCancelledSubscription on Order {
id
avataxId: metafield(key: "avataxId")
channel {
id
slug
}
}
fragment OrderCancelledEventSubscription on Event {
__typename
... on OrderCancelled {
order {
...OrderCancelledSubscription
}
recipient {
privateMetadata {
key
value
}
}
}
}
subscription OrderCancelledSubscription {
event {
...OrderCancelledEventSubscription
}
}

View file

@ -20,7 +20,7 @@ fragment OrderLine on OrderLine {
} }
} }
fragment OrderCreatedSubscription on Order { fragment OrderConfirmedSubscription on Order {
id id
user { user {
id id
@ -64,13 +64,15 @@ fragment OrderCreatedSubscription on Order {
amount amount
} }
} }
avataxEntityCode: metafield(key: "avataxEntityCode") avataxEntityCode: privateMetafield(key: "avataxEntityCode")
avataxTaxCalculationDate: privateMetafield(key: "avataxTaxCalculationDate")
avataxDocumentCode: privateMetafield(key: "avataxDocumentCode")
} }
fragment OrderCreatedEventSubscription on Event { fragment OrderConfirmedEventSubscription on Event {
__typename __typename
... on OrderCreated { ... on OrderConfirmed {
order { order {
...OrderCreatedSubscription ...OrderConfirmedSubscription
} }
} }
recipient { recipient {
@ -80,8 +82,8 @@ fragment OrderCreatedEventSubscription on Event {
} }
} }
} }
subscription OrderCreatedSubscription { subscription OrderConfirmedSubscription {
event { event {
...OrderCreatedEventSubscription ...OrderConfirmedEventSubscription
} }
} }

View file

@ -47,10 +47,7 @@ fragment OrderFulfilledSubscription on Order {
amount amount
} }
} }
privateMetadata { avataxId: metafield(key: "avataxId")
key
value
}
lines { lines {
...OrderLine ...OrderLine
} }

View file

@ -25,7 +25,7 @@
"@trpc/react-query": "10.34.0", "@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0", "@trpc/server": "10.34.0",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"avatax": "^23.3.2", "avatax": "^23.7.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"graphql": "16.7.1", "graphql": "16.7.1",

View file

@ -1,6 +1,6 @@
import { OrderCreatedSubscriptionFragment, OrderStatus } from "../generated/graphql"; import { OrderConfirmedSubscriptionFragment, OrderStatus } from "../generated/graphql";
export const defaultOrder: OrderCreatedSubscriptionFragment = { export const defaultOrder: OrderConfirmedSubscriptionFragment = {
id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2", id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2",
user: { user: {
id: "VXNlcjoyMDg0NTEwNDEw", id: "VXNlcjoyMDg0NTEwNDEw",

View file

@ -3,7 +3,7 @@ import { Client, gql } from "urql";
import { import {
FetchAppDetailsDocument, FetchAppDetailsDocument,
FetchAppDetailsQuery, FetchAppDetailsQuery,
UpdateMetadataDocument, UpdatePrivateMetadataDocument,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
gql` gql`
@ -45,7 +45,7 @@ export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]>
export async function mutateMetadata(client: Client, metadata: MetadataEntry[], appId: string) { export async function mutateMetadata(client: Client, metadata: MetadataEntry[], appId: string) {
const { error: mutationError, data: mutationData } = await client const { error: mutationError, data: mutationData } = await client
.mutation(UpdateMetadataDocument, { .mutation(UpdatePrivateMetadataDocument, {
id: appId, id: appId,
input: metadata, input: metadata,
}) })

View file

@ -0,0 +1,39 @@
import { Client } from "urql";
import {
UpdatePublicMetadataDocument,
UpdatePublicMetadataMutation,
UpdatePublicMetadataMutationVariables,
} from "../../../generated/graphql";
const PROVIDER_ORDER_ID_KEY = "avataxId";
export class OrderMetadataManager {
private privateOrderIdKey = PROVIDER_ORDER_ID_KEY;
constructor(private client: Client) {}
/**
* We need to store the provider order id in the Saleor order metadata so that we can
* update the provider order when the Saleor order is fulfilled.
*/
async updateOrderMetadataWithExternalId(orderId: string, externalId: string) {
const variables: UpdatePublicMetadataMutationVariables = {
id: orderId,
input: [
{
key: this.privateOrderIdKey,
value: externalId,
},
],
};
const { error } = await this.client
.mutation<UpdatePublicMetadataMutation>(UpdatePublicMetadataDocument, variables)
.toPromise();
if (error) {
throw error;
}
return { ok: true };
}
}

View file

@ -1,36 +1,58 @@
import { NextApiResponse } from "next"; import { NextApiResponse } from "next";
import { AvalaraError } from "avatax/lib/AvaTaxClient";
import { ZodError } from "zod";
import { createLogger, Logger } from "../../lib/logger"; import { createLogger, Logger } from "../../lib/logger";
class WebhookErrorResolver {
private logger: Logger;
constructor() {
this.logger = createLogger({ event: "WebhookErrorResolver" });
}
private resolveErrorMessage(error: unknown) {
if (error instanceof ZodError) {
this.logger.error(error.message, "Unexpected Zod error caught:");
this.logger.debug(error.stack, "Error details:");
return error.message;
}
if (error instanceof AvalaraError) {
this.logger.error(error.message, "Unexpected Avalara error caught:");
this.logger.debug(error.stack, "Error stack:");
this.logger.debug(error.target, "Error target:");
return error.message;
}
if (error instanceof Error) {
this.logger.error(error.stack, "Unexpected error caught:");
return error.message;
}
return "Internal server error";
}
resolve(error: unknown) {
return this.resolveErrorMessage(error);
}
}
export class WebhookResponse { export class WebhookResponse {
private logger: Logger; private logger: Logger;
constructor(private res: NextApiResponse) { constructor(private res: NextApiResponse) {
this.logger = createLogger({ event: "WebhookResponse" }); this.logger = createLogger({ event: "WebhookResponse" });
} }
private returnSuccess(data?: unknown) { error(error: unknown) {
this.logger.debug({ data }, "Responding to Saleor with data:"); const errorResolver = new WebhookErrorResolver();
return this.res.status(200).json(data ?? {}); const errorMessage = errorResolver.resolve(error);
}
private returnError(errorMessage: string) {
this.logger.debug({ errorMessage }, "Responding to Saleor with error:"); this.logger.debug({ errorMessage }, "Responding to Saleor with error:");
return this.res.status(500).json({ error: errorMessage }); return this.res.status(500).json({ error: errorMessage });
} }
private resolveError(error: unknown) {
if (error instanceof Error) {
this.logger.error(error.stack, "Unexpected error caught:");
return this.returnError(error.message);
}
return this.returnError("Internal server error");
}
error(error: unknown) {
return this.resolveError(error);
}
success(data?: unknown) { success(data?: unknown) {
return this.returnSuccess(data); return this.res.status(200).json(data ?? {});
} }
} }

View file

@ -3,10 +3,11 @@ import { DocumentType } from "avatax/lib/enums/DocumentType";
import { AddressLocationInfo as AvataxAddress } from "avatax/lib/models/AddressLocationInfo"; import { AddressLocationInfo as AvataxAddress } from "avatax/lib/models/AddressLocationInfo";
import { CommitTransactionModel } from "avatax/lib/models/CommitTransactionModel"; import { CommitTransactionModel } from "avatax/lib/models/CommitTransactionModel";
import { CreateTransactionModel } from "avatax/lib/models/CreateTransactionModel"; import { CreateTransactionModel } from "avatax/lib/models/CreateTransactionModel";
import { LogOptions } from "avatax/lib/utils/logger";
import packageJson from "../../../package.json"; import packageJson from "../../../package.json";
import { createLogger, Logger } from "../../lib/logger";
import { AvataxClientTaxCodeService } from "./avatax-client-tax-code.service"; import { AvataxClientTaxCodeService } from "./avatax-client-tax-code.service";
import { BaseAvataxConfig } from "./avatax-connection-schema"; import { BaseAvataxConfig } from "./avatax-connection-schema";
import { VoidReasonCode } from "avatax/lib/enums/VoidReasonCode";
type AvataxSettings = { type AvataxSettings = {
appName: string; appName: string;
@ -14,11 +15,7 @@ type AvataxSettings = {
environment: "sandbox" | "production"; environment: "sandbox" | "production";
machineName: string; machineName: string;
timeout: number; timeout: number;
logOptions?: { logOptions?: LogOptions;
logEnabled: boolean;
logLevel: number;
logRequestAndResponseInfo: boolean;
};
}; };
const defaultAvataxSettings: AvataxSettings = { const defaultAvataxSettings: AvataxSettings = {
@ -53,12 +50,15 @@ export type ValidateAddressArgs = {
address: AvataxAddress; address: AvataxAddress;
}; };
export type VoidTransactionArgs = {
transactionCode: string;
companyCode: string;
};
export class AvataxClient { export class AvataxClient {
private client: Avatax; private client: Avatax;
private logger: Logger;
constructor(baseConfig: BaseAvataxConfig) { constructor(baseConfig: BaseAvataxConfig) {
this.logger = createLogger({ name: "AvataxClient" });
const settings = createAvataxSettings({ isSandbox: baseConfig.isSandbox }); const settings = createAvataxSettings({ isSandbox: baseConfig.isSandbox });
const avataxClient = new Avatax(settings).withSecurity(baseConfig.credentials); const avataxClient = new Avatax(settings).withSecurity(baseConfig.credentials);
@ -73,6 +73,20 @@ export class AvataxClient {
return this.client.commitTransaction(args); return this.client.commitTransaction(args);
} }
async voidTransaction({
transactionCode,
companyCode,
}: {
transactionCode: string;
companyCode: string;
}) {
return this.client.voidTransaction({
transactionCode,
companyCode,
model: { code: VoidReasonCode.DocVoided },
});
}
async validateAddress({ address }: ValidateAddressArgs) { async validateAddress({ address }: ValidateAddressArgs) {
return this.client.resolveAddress(address); return this.client.resolveAddress(address);
} }

View file

@ -24,7 +24,7 @@ export type BaseAvataxConfig = z.infer<typeof baseAvataxConfigSchema>;
export const avataxConfigSchema = z export const avataxConfigSchema = z
.object({ .object({
name: z.string().min(1, { message: "Name requires at least one character." }), name: z.string().min(1, { message: "Name requires at least one character." }),
companyCode: z.string().optional(), companyCode: z.string(),
isAutocommit: z.boolean(), isAutocommit: z.boolean(),
shippingTaxCode: z.string().optional(), shippingTaxCode: z.string().optional(),
isDocumentRecordingEnabled: z.boolean().default(true), isDocumentRecordingEnabled: z.boolean().default(true),
@ -36,7 +36,7 @@ export type AvataxConfig = z.infer<typeof avataxConfigSchema>;
export const defaultAvataxConfig: AvataxConfig = { export const defaultAvataxConfig: AvataxConfig = {
name: "", name: "",
companyCode: "", companyCode: "DEFAULT",
isSandbox: false, isSandbox: false,
isAutocommit: false, isAutocommit: false,
isDocumentRecordingEnabled: true, isDocumentRecordingEnabled: true,

View file

@ -1,16 +1,17 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { import {
OrderCreatedSubscriptionFragment, OrderConfirmedSubscriptionFragment,
OrderFulfilledSubscriptionFragment, OrderFulfilledSubscriptionFragment,
TaxBaseFragment, TaxBaseFragment,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
import { Logger, createLogger } from "../../lib/logger"; import { Logger, createLogger } from "../../lib/logger";
import { ProviderWebhookService } from "../taxes/tax-provider-webhook"; import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
import { CreateOrderResponse, ProviderWebhookService } from "../taxes/tax-provider-webhook";
import { AvataxClient } from "./avatax-client"; import { AvataxClient } from "./avatax-client";
import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema"; 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 { AvataxOrderCreatedAdapter } from "./order-created/avatax-order-created-adapter"; import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
import { AvataxOrderFulfilledAdapter } from "./order-fulfilled/avatax-order-fulfilled-adapter"; import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
export class AvataxWebhookService implements ProviderWebhookService { export class AvataxWebhookService implements ProviderWebhookService {
config = defaultAvataxConfig; config = defaultAvataxConfig;
@ -35,19 +36,17 @@ export class AvataxWebhookService implements ProviderWebhookService {
return response; return response;
} }
async createOrder(order: OrderCreatedSubscriptionFragment) { async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
const adapter = new AvataxOrderCreatedAdapter(this.config, this.authData); const adapter = new AvataxOrderConfirmedAdapter(this.config, this.authData);
const response = await adapter.send({ order }); const response = await adapter.send({ order });
return response; return response;
} }
async fulfillOrder(order: OrderFulfilledSubscriptionFragment) { async cancelOrder(payload: OrderCancelledPayload) {
const adapter = new AvataxOrderFulfilledAdapter(this.config); const adapter = new AvataxOrderCancelledAdapter(this.config);
const response = await adapter.send({ order }); await adapter.send(payload);
return response;
} }
} }

View file

@ -0,0 +1,31 @@
import { Logger, createLogger } from "../../../lib/logger";
import { OrderCancelledPayload } from "../../../pages/api/webhooks/order-cancelled";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient, VoidTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer";
export type AvataxOrderCancelledTarget = VoidTransactionArgs;
export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelledPayload, void> {
private logger: Logger;
constructor(private readonly config: AvataxConfig) {
this.logger = createLogger({ name: "AvataxOrderCancelledAdapter" });
}
async send(payload: OrderCancelledPayload) {
this.logger.debug("Transforming the Saleor payload for cancelling transaction with Avatax...");
const payloadTransformer = new AvataxOrderCancelledPayloadTransformer(this.config);
const target = payloadTransformer.transform({ ...payload });
this.logger.debug("Calling Avatax voidTransaction with transformed payload...");
const client = new AvataxClient(this.config);
await client.voidTransaction(target);
this.logger.debug(`Succesfully voided the transaction of id: ${target.transactionCode}`);
}
}

View file

@ -0,0 +1,30 @@
import { OrderCancelledPayload } from "../../../pages/api/webhooks/order-cancelled";
import { AvataxConfigMockGenerator } from "../avatax-config-mock-generator";
import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer";
import { describe, expect, it } from "vitest";
const configMockGenerator = new AvataxConfigMockGenerator();
const avataxMockConfig = configMockGenerator.generateAvataxConfig();
describe("AvataxOrderCancelledPayloadTransformer", () => {
it("throws an error when order = null", () => {
const payload = { order: null } as any as OrderCancelledPayload;
const transformer = new AvataxOrderCancelledPayloadTransformer(avataxMockConfig);
expect(() => transformer.transform(payload)).toThrow("Order is required");
});
it("throws an error when no avataxId is present", () => {
const payload = { order: {} } as any as OrderCancelledPayload;
const transformer = new AvataxOrderCancelledPayloadTransformer(avataxMockConfig);
expect(() => transformer.transform(payload)).toThrow();
});
it("returns a valid AvataxOrderCancelledTarget", () => {
const payload = { order: { avataxId: "123" } } as any as OrderCancelledPayload;
const transformer = new AvataxOrderCancelledPayloadTransformer(avataxMockConfig);
const target = transformer.transform(payload);
expect(target).toEqual({ transactionCode: "123", companyCode: "DEFAULT" });
});
});

View file

@ -0,0 +1,21 @@
import { z } from "zod";
import { OrderCancelledPayload } from "../../../pages/api/webhooks/order-cancelled";
import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxOrderCancelledTarget } from "./avatax-order-cancelled-adapter";
export class AvataxOrderCancelledPayloadTransformer {
constructor(private readonly config: AvataxConfig) {}
transform({ order }: OrderCancelledPayload): AvataxOrderCancelledTarget {
if (!order) {
throw new Error("Order is required");
}
const transactionCode = z.string().min(1).parse(order.avataxId);
return {
transactionCode,
companyCode: this.config.companyCode ?? "", // If companyCode is not defined, we set the value to empty string so that the default company is used.
};
}
}

View file

@ -1,6 +1,6 @@
import { describe, it } from "vitest"; import { describe, it } from "vitest";
describe("AvataxOrderCreatedAdapter", () => { describe("AvataxOrderConfirmedAdapter", () => {
// ? how to mock internal call to avatax? // ? how to mock internal call to avatax?
it.todo("calls avatax client", () => {}); it.todo("calls avatax client", () => {});
}); });

View file

@ -1,32 +1,31 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { Logger, createLogger } from "../../../lib/logger"; import { Logger, createLogger } from "../../../lib/logger";
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient } from "../avatax-client"; import { AvataxClient } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer"; import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service";
import { AvataxOrderCreatedPayloadService } from "./avatax-order-created-payload.service"; import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer";
type AvataxOrderCreatedPayload = { type AvataxOrderConfirmedPayload = {
order: OrderCreatedSubscriptionFragment; order: OrderConfirmedSubscriptionFragment;
}; };
type AvataxOrderCreatedResponse = CreateOrderResponse; type AvataxOrderConfirmedResponse = CreateOrderResponse;
export class AvataxOrderCreatedAdapter export class AvataxOrderConfirmedAdapter
implements WebhookAdapter<AvataxOrderCreatedPayload, AvataxOrderCreatedResponse> implements WebhookAdapter<AvataxOrderConfirmedPayload, AvataxOrderConfirmedResponse>
{ {
private logger: Logger; private logger: Logger;
constructor(private readonly config: AvataxConfig, private authData: AuthData) { constructor(private readonly config: AvataxConfig, private authData: AuthData) {
this.logger = createLogger({ name: "AvataxOrderCreatedAdapter" }); this.logger = createLogger({ name: "AvataxOrderConfirmedAdapter" });
} }
async send(payload: AvataxOrderCreatedPayload): Promise<AvataxOrderCreatedResponse> { async send(payload: AvataxOrderConfirmedPayload): Promise<AvataxOrderConfirmedResponse> {
this.logger.debug("Transforming the Saleor payload for creating order with Avatax..."); this.logger.debug("Transforming the Saleor payload for creating order with Avatax...");
const payloadService = new AvataxOrderCreatedPayloadService(this.authData); const payloadService = new AvataxOrderConfirmedPayloadService(this.authData);
const target = await payloadService.getPayload(payload.order, this.config); const target = await payloadService.getPayload(payload.order, this.config);
this.logger.debug("Calling Avatax createTransaction with transformed payload..."); this.logger.debug("Calling Avatax createTransaction with transformed payload...");
@ -36,7 +35,7 @@ export class AvataxOrderCreatedAdapter
this.logger.debug("Avatax createTransaction successfully responded"); this.logger.debug("Avatax createTransaction successfully responded");
const responseTransformer = new AvataxOrderCreatedResponseTransformer(); const responseTransformer = new AvataxOrderConfirmedResponseTransformer();
const transformedResponse = responseTransformer.transform(response); const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed Avatax createTransaction response"); this.logger.debug("Transformed Avatax createTransaction response");

View file

@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { AvataxOrderConfirmedCalculationDateResolver } from "./avatax-order-confirmed-calculation-date-resolver";
const resolver = new AvataxOrderConfirmedCalculationDateResolver();
describe("AvataxOrderConfirmedCalculationDateResolver", () => {
it("should return the metadata tax calculation date if it is set", () => {
const order = {
avataxTaxCalculationDate: "2021-01-01T00:00:00.000Z",
created: "2021-01-02T00:00:00.000Z",
} as any as OrderConfirmedSubscriptionFragment;
expect(resolver.resolve(order)).toEqual(new Date("2021-01-01T00:00:00.000Z"));
});
it("should fallback to order created when metadata tax calculation date is not a string datetime", () => {
const order = {
avataxTaxCalculationDate: "not-a-datetime",
created: "2021-01-02T00:00:00.000Z",
} as any as OrderConfirmedSubscriptionFragment;
expect(resolver.resolve(order)).toEqual(new Date("2021-01-02T00:00:00.000Z"));
});
it("should return the order creation date if the metadata tax calculation date is not set", () => {
const order = {
created: "2021-01-02T00:00:00.000Z",
} as any as OrderConfirmedSubscriptionFragment;
expect(resolver.resolve(order)).toEqual(new Date("2021-01-02T00:00:00.000Z"));
});
});

View file

@ -0,0 +1,33 @@
import { z } from "zod";
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { createLogger } from "../../../lib/logger";
export class AvataxOrderConfirmedCalculationDateResolver {
private logger = createLogger({
name: "AvataxOrderConfirmedCalculationDateResolver",
});
resolve(order: OrderConfirmedSubscriptionFragment): Date {
if (!order.avataxTaxCalculationDate) {
this.logger.info("No tax calculation date provided. Falling back to order created date.");
return new Date(order.created);
}
// UTC datetime string, e.g. "2021-08-31T13:00:00.000Z"
const taxCalculationParse = z.string().datetime().safeParse(order.avataxTaxCalculationDate);
if (taxCalculationParse.success) {
// The user is able to pass other tax calculation date than the order creation date.
this.logger.info(
"Valid UTC tax calculation date found in metadata. Using it for tax calculation."
);
return new Date(taxCalculationParse.data);
} else {
this.logger.warn(
`The tax calculation date ${order.avataxTaxCalculationDate} is not a valid UTC datetime. Falling back to order created date.`
);
return new Date(order.created);
}
}
}

View file

@ -0,0 +1,23 @@
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { AvataxOrderConfirmedDocumentCodeResolver } from "./avatax-order-confirmed-document-code-resolver";
import { expect, describe, it } from "vitest";
const resolver = new AvataxOrderConfirmedDocumentCodeResolver();
describe("AvataxOrderConfirmedDocumentCodeResolver", () => {
it("returns document code when provided in metadata", () => {
expect(
resolver.resolve({
id: "id",
avataxDocumentCode: "123",
} as unknown as OrderConfirmedSubscriptionFragment)
).toBe("123");
});
it("returns order id when document code is not provided in metadata", () => {
expect(
resolver.resolve({
id: "id",
} as unknown as OrderConfirmedSubscriptionFragment)
).toBe("id");
});
});

View file

@ -0,0 +1,11 @@
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
export class AvataxOrderConfirmedDocumentCodeResolver {
resolve(order: OrderConfirmedSubscriptionFragment): string {
/*
* The value for "code" can be provided in the metadata.
* Read more: https://developer.avalara.com/erp-integration-guide/sales-tax-badge/transactions/cert-document-codes/
*/
return order.avataxDocumentCode ?? order.id;
}
}

View file

@ -1,7 +1,7 @@
import { TransactionModel } from "avatax/lib/models/TransactionModel"; import { TransactionModel } from "avatax/lib/models/TransactionModel";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { ChannelConfig } from "../../channel-configuration/channel-config"; import { ChannelConfig } from "../../channel-configuration/channel-config";
import { orderCreatedTransactionMock } from "./avatax-order-created-response-transaction-mock"; import { orderConfirmedTransactionMock } from "./avatax-order-confirmed-response-transaction-mock";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { defaultOrder } from "../../../mocks"; import { defaultOrder } from "../../../mocks";
import { AvataxConfigMockGenerator } from "../avatax-config-mock-generator"; import { AvataxConfigMockGenerator } from "../avatax-config-mock-generator";
@ -14,23 +14,23 @@ const defaultChannelConfig: ChannelConfig = {
}, },
}; };
const defaultOrderCreatedResponse: TransactionModel = orderCreatedTransactionMock; const defaultOrderConfirmedResponse: TransactionModel = orderConfirmedTransactionMock;
const testingScenariosMap = { const testingScenariosMap = {
default: { default: {
order: defaultOrder, order: defaultOrder,
channelConfig: defaultChannelConfig, channelConfig: defaultChannelConfig,
response: defaultOrderCreatedResponse, response: defaultOrderConfirmedResponse,
}, },
}; };
type TestingScenario = keyof typeof testingScenariosMap; type TestingScenario = keyof typeof testingScenariosMap;
export class AvataxOrderCreatedMockGenerator { export class AvataxOrderConfirmedMockGenerator {
constructor(private scenario: TestingScenario = "default") {} constructor(private scenario: TestingScenario = "default") {}
generateOrder = ( generateOrder = (
overrides: Partial<OrderCreatedSubscriptionFragment> = {} overrides: Partial<OrderConfirmedSubscriptionFragment> = {}
): OrderCreatedSubscriptionFragment => ): OrderConfirmedSubscriptionFragment =>
structuredClone({ structuredClone({
...testingScenariosMap[this.scenario].order, ...testingScenariosMap[this.scenario].order,
...overrides, ...overrides,

View file

@ -1,16 +1,16 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer"; import { AvataxOrderConfirmedPayloadLinesTransformer } from "./avatax-order-confirmed-payload-lines-transformer";
import { avataxConfigMock } from "./avatax-order-created-payload-transformer.test"; import { avataxConfigMock } from "./avatax-order-confirmed-payload-transformer.test";
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator"; import { AvataxOrderConfirmedMockGenerator } from "./avatax-order-confirmed-mock-generator";
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer(); const linesTransformer = new AvataxOrderConfirmedPayloadLinesTransformer();
const mockGenerator = new AvataxOrderCreatedMockGenerator(); const mockGenerator = new AvataxOrderConfirmedMockGenerator();
const orderMock = mockGenerator.generateOrder(); const orderMock = mockGenerator.generateOrder();
const matches: AvataxTaxCodeMatches = []; const matches: AvataxTaxCodeMatches = [];
describe("AvataxOrderCreatedPayloadLinesTransformer", () => { describe("AvataxOrderConfirmedPayloadLinesTransformer", () => {
const lines = linesTransformer.transform(orderMock, avataxConfigMock, matches); const lines = linesTransformer.transform(orderMock, avataxConfigMock, matches);
it("returns the correct number of lines", () => { it("returns the correct number of lines", () => {

View file

@ -1,19 +1,19 @@
import { LineItemModel } from "avatax/lib/models/LineItemModel"; import { LineItemModel } from "avatax/lib/models/LineItemModel";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { numbers } from "../../taxes/numbers"; import { numbers } from "../../taxes/numbers";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
import { SHIPPING_ITEM_CODE } from "./avatax-order-created-payload-transformer"; import { SHIPPING_ITEM_CODE } from "./avatax-order-confirmed-payload-transformer";
import { AvataxOrderCreatedTaxCodeMatcher } from "./avatax-order-created-tax-code-matcher"; import { AvataxOrderConfirmedTaxCodeMatcher } from "./avatax-order-confirmed-tax-code-matcher";
export class AvataxOrderCreatedPayloadLinesTransformer { export class AvataxOrderConfirmedPayloadLinesTransformer {
transform( transform(
order: OrderCreatedSubscriptionFragment, order: OrderConfirmedSubscriptionFragment,
config: AvataxConfig, config: AvataxConfig,
matches: AvataxTaxCodeMatches matches: AvataxTaxCodeMatches
): LineItemModel[] { ): LineItemModel[] {
const productLines: LineItemModel[] = order.lines.map((line) => { const productLines: LineItemModel[] = order.lines.map((line) => {
const matcher = new AvataxOrderCreatedTaxCodeMatcher(); const matcher = new AvataxOrderConfirmedTaxCodeMatcher();
const taxCode = matcher.match(line, matches); const taxCode = matcher.match(line, matches);
return { return {

View file

@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator"; import { AvataxOrderConfirmedMockGenerator } from "./avatax-order-confirmed-mock-generator";
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer"; import { AvataxOrderConfirmedPayloadTransformer } from "./avatax-order-confirmed-payload-transformer";
import { DocumentType } from "avatax/lib/enums/DocumentType"; import { DocumentType } from "avatax/lib/enums/DocumentType";
const mockGenerator = new AvataxOrderCreatedMockGenerator(); const mockGenerator = new AvataxOrderConfirmedMockGenerator();
const orderMock = mockGenerator.generateOrder(); const orderMock = mockGenerator.generateOrder();
const discountedOrderMock = mockGenerator.generateOrder({ const discountedOrderMock = mockGenerator.generateOrder({
@ -17,11 +17,11 @@ const discountedOrderMock = mockGenerator.generateOrder({
], ],
}); });
const transformer = new AvataxOrderCreatedPayloadTransformer(); const transformer = new AvataxOrderConfirmedPayloadTransformer();
export const avataxConfigMock = mockGenerator.generateAvataxConfig(); export const avataxConfigMock = mockGenerator.generateAvataxConfig();
describe("AvataxOrderCreatedPayloadTransformer", () => { describe("AvataxOrderConfirmedPayloadTransformer", () => {
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", async () => { it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", async () => {
const payload = await transformer.transform(orderMock, avataxConfigMock, []); const payload = await transformer.transform(orderMock, avataxConfigMock, []);
@ -48,7 +48,7 @@ describe("AvataxOrderCreatedPayloadTransformer", () => {
expect(check).toBe(true); expect(check).toBe(true);
}); });
it("returns lines with discounted: false when there are no discounts", async () => { it("returns lines with discounted: false when there are no discounts", async () => {
const transformer = new AvataxOrderCreatedPayloadTransformer(); const transformer = new AvataxOrderConfirmedPayloadTransformer();
const payload = await transformer.transform(orderMock, avataxConfigMock, []); const payload = await transformer.transform(orderMock, avataxConfigMock, []);
const linesWithoutShipping = payload.model.lines.slice(0, -1); const linesWithoutShipping = payload.model.lines.slice(0, -1);

View file

@ -1,16 +1,18 @@
import { DocumentType } from "avatax/lib/enums/DocumentType"; import { DocumentType } from "avatax/lib/enums/DocumentType";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { discountUtils } from "../../taxes/discount-utils"; import { discountUtils } from "../../taxes/discount-utils";
import { avataxAddressFactory } from "../address-factory"; import { avataxAddressFactory } from "../address-factory";
import { AvataxClient, CreateTransactionArgs } from "../avatax-client"; import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
import { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer"; import { AvataxOrderConfirmedPayloadLinesTransformer } from "./avatax-order-confirmed-payload-lines-transformer";
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher"; import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
import { AvataxOrderConfirmedCalculationDateResolver } from "./avatax-order-confirmed-calculation-date-resolver";
import { AvataxOrderConfirmedDocumentCodeResolver } from "./avatax-order-confirmed-document-code-resolver";
export const SHIPPING_ITEM_CODE = "Shipping"; export const SHIPPING_ITEM_CODE = "Shipping";
export class AvataxOrderCreatedPayloadTransformer { export class AvataxOrderConfirmedPayloadTransformer {
private matchDocumentType(config: AvataxConfig): DocumentType { private matchDocumentType(config: AvataxConfig): DocumentType {
if (!config.isDocumentRecordingEnabled) { if (!config.isDocumentRecordingEnabled) {
// isDocumentRecordingEnabled = false changes all the DocTypes within your AvaTax requests to SalesOrder. This will stop any transaction from being recorded within AvaTax. // isDocumentRecordingEnabled = false changes all the DocTypes within your AvaTax requests to SalesOrder. This will stop any transaction from being recorded within AvaTax.
@ -19,18 +21,26 @@ export class AvataxOrderCreatedPayloadTransformer {
return DocumentType.SalesInvoice; return DocumentType.SalesInvoice;
} }
async transform( async transform(
order: OrderCreatedSubscriptionFragment, order: OrderConfirmedSubscriptionFragment,
avataxConfig: AvataxConfig, avataxConfig: AvataxConfig,
matches: AvataxTaxCodeMatches matches: AvataxTaxCodeMatches
): Promise<CreateTransactionArgs> { ): Promise<CreateTransactionArgs> {
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
const avataxClient = new AvataxClient(avataxConfig); const avataxClient = new AvataxClient(avataxConfig);
const linesTransformer = new AvataxOrderConfirmedPayloadLinesTransformer();
const dateResolver = new AvataxOrderConfirmedCalculationDateResolver();
const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient }); const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient });
const documentCodeResolver = new AvataxOrderConfirmedDocumentCodeResolver();
const entityUseCode = await entityTypeMatcher.match(order.avataxEntityCode); const entityUseCode = await entityTypeMatcher.match(order.avataxEntityCode);
const date = dateResolver.resolve(order);
const code = documentCodeResolver.resolve(order);
return { return {
model: { model: {
code,
type: this.matchDocumentType(avataxConfig), type: this.matchDocumentType(avataxConfig),
entityUseCode, entityUseCode,
customerCode: customerCode:
@ -47,7 +57,7 @@ export class AvataxOrderCreatedPayloadTransformer {
currencyCode: order.total.currency, currencyCode: order.total.currency,
email: order.user?.email ?? "", email: order.user?.email ?? "",
lines: linesTransformer.transform(order, avataxConfig, matches), lines: linesTransformer.transform(order, avataxConfig, matches),
date: new Date(order.created), date,
discount: discountUtils.sumDiscounts( discount: discountUtils.sumDiscounts(
order.discounts.map((discount) => discount.amount.amount) order.discounts.map((discount) => discount.amount.amount)
), ),

View file

@ -1,11 +1,11 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { CreateTransactionArgs } from "../avatax-client"; import { CreateTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxTaxCodeMatchesService } from "../tax-code/avatax-tax-code-matches.service"; import { AvataxTaxCodeMatchesService } from "../tax-code/avatax-tax-code-matches.service";
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer"; import { AvataxOrderConfirmedPayloadTransformer } from "./avatax-order-confirmed-payload-transformer";
export class AvataxOrderCreatedPayloadService { export class AvataxOrderConfirmedPayloadService {
constructor(private authData: AuthData) {} constructor(private authData: AuthData) {}
private getMatches() { private getMatches() {
@ -15,11 +15,11 @@ export class AvataxOrderCreatedPayloadService {
} }
async getPayload( async getPayload(
order: OrderCreatedSubscriptionFragment, order: OrderConfirmedSubscriptionFragment,
avataxConfig: AvataxConfig avataxConfig: AvataxConfig
): Promise<CreateTransactionArgs> { ): Promise<CreateTransactionArgs> {
const matches = await this.getMatches(); const matches = await this.getMatches();
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer(); const payloadTransformer = new AvataxOrderConfirmedPayloadTransformer();
return payloadTransformer.transform(order, avataxConfig, matches); return payloadTransformer.transform(order, avataxConfig, matches);
} }

View file

@ -1,6 +1,6 @@
import { TransactionModel } from "avatax/lib/models/TransactionModel"; import { TransactionModel } from "avatax/lib/models/TransactionModel";
export const orderCreatedTransactionMock = { export const orderConfirmedTransactionMock = {
id: 85026453274669, id: 85026453274669,
code: "31fa9e94-e8df-429b-b303-549e7bc10f1e", code: "31fa9e94-e8df-429b-b303-549e7bc10f1e",
companyId: 7799660, companyId: 7799660,

View file

@ -9,7 +9,7 @@ import { LiabilityType } from "avatax/lib/enums/LiabilityType";
import { RateType } from "avatax/lib/enums/RateType"; import { RateType } from "avatax/lib/enums/RateType";
import { TransactionModel } from "avatax/lib/models/TransactionModel"; import { TransactionModel } from "avatax/lib/models/TransactionModel";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer"; import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
const MOCKED_TRANSACTION: TransactionModel = { const MOCKED_TRANSACTION: TransactionModel = {
id: 0, id: 0,
@ -497,9 +497,9 @@ const MOCKED_TRANSACTION: TransactionModel = {
], ],
}; };
describe("AvataxOrderCreatedResponseTransformer", () => { describe("AvataxOrderConfirmedResponseTransformer", () => {
it("returns orded id in response", () => { it("returns orded id in response", () => {
const transformer = new AvataxOrderCreatedResponseTransformer(); const transformer = new AvataxOrderConfirmedResponseTransformer();
const result = transformer.transform(MOCKED_TRANSACTION); const result = transformer.transform(MOCKED_TRANSACTION);
expect(result).toEqual({ expect(result).toEqual({
@ -507,7 +507,7 @@ describe("AvataxOrderCreatedResponseTransformer", () => {
}); });
}); });
it("throws an error when no transaction id is present", () => { it("throws an error when no transaction id is present", () => {
const transformer = new AvataxOrderCreatedResponseTransformer(); const transformer = new AvataxOrderConfirmedResponseTransformer();
expect(() => transformer.transform({} as any)).toThrowError(); expect(() => transformer.transform({} as any)).toThrowError();
}); });

View file

@ -2,7 +2,7 @@ import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
import { TransactionModel } from "avatax/lib/models/TransactionModel"; import { TransactionModel } from "avatax/lib/models/TransactionModel";
import { taxProviderUtils } from "../../taxes/tax-provider-utils"; import { taxProviderUtils } from "../../taxes/tax-provider-utils";
export class AvataxOrderCreatedResponseTransformer { export class AvataxOrderConfirmedResponseTransformer {
transform(response: TransactionModel): CreateOrderResponse { transform(response: TransactionModel): CreateOrderResponse {
return { return {
id: taxProviderUtils.resolveOptionalOrThrow( id: taxProviderUtils.resolveOptionalOrThrow(

View file

@ -1,7 +1,7 @@
import { OrderLineFragment } from "../../../../generated/graphql"; import { OrderLineFragment } from "../../../../generated/graphql";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
import { AvataxOrderCreatedTaxCodeMatcher } from "./avatax-order-created-tax-code-matcher"; import { AvataxOrderConfirmedTaxCodeMatcher } from "./avatax-order-confirmed-tax-code-matcher";
const mockedLine: OrderLineFragment = { const mockedLine: OrderLineFragment = {
productSku: "sku", productSku: "sku",
@ -42,15 +42,15 @@ const matches: AvataxTaxCodeMatches = [
}, },
]; ];
describe("AvataxOrderCreatedTaxCodeMatcher", () => { describe("AvataxOrderConfirmedTaxCodeMatcher", () => {
it("should return empty string if tax class is not found", () => { it("should return empty string if tax class is not found", () => {
const matcher = new AvataxOrderCreatedTaxCodeMatcher(); const matcher = new AvataxOrderConfirmedTaxCodeMatcher();
expect(matcher.match(mockedLine, matches)).toEqual(""); expect(matcher.match(mockedLine, matches)).toEqual("");
}); });
it("should return tax code if tax class is found", () => { it("should return tax code if tax class is found", () => {
const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } }); const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } });
const matcher = new AvataxOrderCreatedTaxCodeMatcher(); const matcher = new AvataxOrderConfirmedTaxCodeMatcher();
expect(matcher.match(line, matches)).toEqual("P0000000"); expect(matcher.match(line, matches)).toEqual("P0000000");
}); });

View file

@ -1,7 +1,7 @@
import { OrderLineFragment } from "../../../../generated/graphql"; import { OrderLineFragment } from "../../../../generated/graphql";
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
export class AvataxOrderCreatedTaxCodeMatcher { export class AvataxOrderConfirmedTaxCodeMatcher {
private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) { private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) {
return matches.find((m) => m.data.saleorTaxClassId === taxClassId); return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
} }

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,171 +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,
PROVIDER_ORDER_ID_KEY,
getTransactionCodeFromMetadata,
} 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",
},
};
const MOCKED_METADATA: OrderFulfilledSubscriptionFragment["privateMetadata"] = [
{
key: PROVIDER_ORDER_ID_KEY,
value: "transaction-code",
},
];
type OrderFulfilled = OrderFulfilledSubscriptionFragment;
const ORDER_FULFILLED_MOCK: OrderFulfilled = {
id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5",
created: "2023-04-11T11:03:09.304109+00:00",
privateMetadata: MOCKED_METADATA,
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,
},
},
},
],
};
describe("getTransactionCodeFromMetadata", () => {
it("returns transaction code", () => {
expect(getTransactionCodeFromMetadata(MOCKED_METADATA)).toBe("transaction-code");
});
it("throws error when transaction code not found", () => {
expect(() => getTransactionCodeFromMetadata([])).toThrowError();
});
});
const MOCKED_ORDER_FULFILLED_PAYLOAD: {
order: OrderFulfilledSubscriptionFragment;
} = {
order: ORDER_FULFILLED_MOCK,
};
describe("AvataxOrderFulfilledPayloadTransformer", () => {
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,46 +0,0 @@
import { DocumentType } from "avatax/lib/enums/DocumentType";
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
import { AvataxConfig } from "../avatax-connection-schema";
import {
AvataxOrderFulfilledPayload,
AvataxOrderFulfilledTarget,
} from "./avatax-order-fulfilled-adapter";
// * This is the key that we use to store the provider order id in the Saleor order metadata.
export const PROVIDER_ORDER_ID_KEY = "externalId";
export function getTransactionCodeFromMetadata(
metadata: OrderFulfilledSubscriptionFragment["privateMetadata"]
) {
const transactionCode = metadata.find((item) => item.key === PROVIDER_ORDER_ID_KEY);
if (!transactionCode) {
throw new Error("Transaction code not found");
}
return transactionCode.value;
}
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 = getTransactionCodeFromMetadata(order.privateMetadata);
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

@ -1,7 +1,8 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { import {
MetadataItem, MetadataItem,
OrderCreatedSubscriptionFragment, OrderCancelledEventSubscriptionFragment,
OrderConfirmedSubscriptionFragment,
OrderFulfilledSubscriptionFragment, OrderFulfilledSubscriptionFragment,
TaxBaseFragment, TaxBaseFragment,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
@ -11,7 +12,8 @@ import { getAppConfig } from "../app/get-app-config";
import { AvataxWebhookService } from "../avatax/avatax-webhook.service"; import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
import { ProviderConnection } from "../provider-connections/provider-connections"; import { ProviderConnection } from "../provider-connections/provider-connections";
import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service"; import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service";
import { ProviderWebhookService } from "./tax-provider-webhook"; import { CreateOrderResponse, ProviderWebhookService } from "./tax-provider-webhook";
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
// todo: refactor to a factory // todo: refactor to a factory
class ActiveTaxProviderService implements ProviderWebhookService { class ActiveTaxProviderService implements ProviderWebhookService {
@ -48,12 +50,12 @@ class ActiveTaxProviderService implements ProviderWebhookService {
return this.client.calculateTaxes(payload); return this.client.calculateTaxes(payload);
} }
async createOrder(order: OrderCreatedSubscriptionFragment) { async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
return this.client.createOrder(order); return this.client.confirmOrder(order);
} }
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) { async cancelOrder(payload: OrderCancelledPayload) {
return this.client.fulfillOrder(payload); this.client.cancelOrder(payload);
} }
} }

View file

@ -3,8 +3,8 @@
* If it ever happens, we have nothing to fall back to, so we throw an error. * If it ever happens, we have nothing to fall back to, so we throw an error.
* Should only be used for values that are required for further calculation. * Should only be used for values that are required for further calculation.
*/ */
function resolveOptionalOrThrow<T>(value: T | undefined, error?: Error): T { function resolveOptionalOrThrow<T>(value: T | undefined | null, error?: Error): T {
if (value === undefined) { if (value === undefined || value === null) {
throw error throw error
? error ? error
: new Error("Could not resolve data. Value needed for further calculation is undefined."); : new Error("Could not resolve data. Value needed for further calculation is undefined.");

View file

@ -1,10 +1,10 @@
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next"; import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
import { import {
OrderCreatedSubscriptionFragment, OrderConfirmedSubscriptionFragment,
OrderFulfilledSubscriptionFragment, OrderFulfilledSubscriptionFragment,
TaxBaseFragment, TaxBaseFragment,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
import { ChannelConfig } from "../channel-configuration/channel-config"; import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"]; export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
@ -12,6 +12,6 @@ export type CreateOrderResponse = { id: string };
export interface ProviderWebhookService { export interface ProviderWebhookService {
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>; calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
createOrder: (payload: OrderCreatedSubscriptionFragment) => Promise<CreateOrderResponse>; confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
fulfillOrder: (payload: OrderFulfilledSubscriptionFragment) => Promise<{ ok: boolean }>; cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
} }

View file

@ -0,0 +1,3 @@
import { describe } from "vitest";
describe.todo("TaxJarOrderConfirmedAdapter", () => {});

View file

@ -1,30 +1,30 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { Logger, createLogger } from "../../../lib/logger"; import { Logger, createLogger } from "../../../lib/logger";
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { CreateOrderArgs, TaxJarClient } from "../taxjar-client"; import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
import { TaxJarConfig } from "../taxjar-connection-schema"; import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarOrderCreatedPayloadService } from "./taxjar-order-created-payload.service"; import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service";
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer"; import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
export type TaxJarOrderCreatedPayload = { export type TaxJarOrderConfirmedPayload = {
order: OrderCreatedSubscriptionFragment; order: OrderConfirmedSubscriptionFragment;
}; };
export type TaxJarOrderCreatedTarget = CreateOrderArgs; export type TaxJarOrderConfirmedTarget = CreateOrderArgs;
export type TaxJarOrderCreatedResponse = CreateOrderResponse; export type TaxJarOrderConfirmedResponse = CreateOrderResponse;
export class TaxJarOrderCreatedAdapter export class TaxJarOrderConfirmedAdapter
implements WebhookAdapter<TaxJarOrderCreatedPayload, TaxJarOrderCreatedResponse> implements WebhookAdapter<TaxJarOrderConfirmedPayload, TaxJarOrderConfirmedResponse>
{ {
private logger: Logger; private logger: Logger;
constructor(private readonly config: TaxJarConfig, private authData: AuthData) { constructor(private readonly config: TaxJarConfig, private authData: AuthData) {
this.logger = createLogger({ name: "TaxJarOrderCreatedAdapter" }); this.logger = createLogger({ name: "TaxJarOrderConfirmedAdapter" });
} }
async send(payload: TaxJarOrderCreatedPayload): Promise<TaxJarOrderCreatedResponse> { async send(payload: TaxJarOrderConfirmedPayload): Promise<TaxJarOrderConfirmedResponse> {
this.logger.debug("Transforming the Saleor payload for creating order with TaxJar..."); this.logger.debug("Transforming the Saleor payload for creating order with TaxJar...");
const payloadService = new TaxJarOrderCreatedPayloadService(this.authData); const payloadService = new TaxJarOrderConfirmedPayloadService(this.authData);
const target = await payloadService.getPayload(payload.order, this.config); const target = await payloadService.getPayload(payload.order, this.config);
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload..."); this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
@ -33,7 +33,7 @@ export class TaxJarOrderCreatedAdapter
const response = await client.createOrder(target); const response = await client.createOrder(target);
this.logger.debug("TaxJar createOrder successfully responded"); this.logger.debug("TaxJar createOrder successfully responded");
const responseTransformer = new TaxJarOrderCreatedResponseTransformer(); const responseTransformer = new TaxJarOrderConfirmedResponseTransformer();
const transformedResponse = responseTransformer.transform(response); const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed TaxJar createOrder response"); this.logger.debug("Transformed TaxJar createOrder response");

View file

@ -1,11 +1,11 @@
import { CreateOrderRes } from "taxjar/dist/types/returnTypes"; import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
import { OrderCreatedSubscriptionFragment, OrderStatus } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { ChannelConfig } from "../../channel-configuration/channel-config";
import { defaultOrder } from "../../../mocks"; import { defaultOrder } from "../../../mocks";
import { TaxJarConfig } from "../taxjar-connection-schema"; import { ChannelConfig } from "../../channel-configuration/channel-config";
import { ChannelConfigMockGenerator } from "../../channel-configuration/channel-config-mock-generator"; import { ChannelConfigMockGenerator } from "../../channel-configuration/channel-config-mock-generator";
import { TaxJarConfig } from "../taxjar-connection-schema";
type Order = OrderCreatedSubscriptionFragment; type Order = OrderConfirmedSubscriptionFragment;
// providerConfigMockGenerator class that other classes extend? // providerConfigMockGenerator class that other classes extend?
const defaultProviderConfig: TaxJarConfig = { const defaultProviderConfig: TaxJarConfig = {
@ -23,7 +23,7 @@ const defaultProviderConfig: TaxJarConfig = {
}, },
}; };
const defaultOrderCreatedResponse: CreateOrderRes = { const defaultOrderConfirmedResponse: CreateOrderRes = {
order: { order: {
user_id: 314973, user_id: 314973,
transaction_reference_id: null, transaction_reference_id: null,
@ -82,14 +82,14 @@ const defaultOrderCreatedResponse: CreateOrderRes = {
const testingScenariosMap = { const testingScenariosMap = {
default: { default: {
order: defaultOrder, order: defaultOrder,
response: defaultOrderCreatedResponse, response: defaultOrderConfirmedResponse,
providerConfig: defaultProviderConfig, providerConfig: defaultProviderConfig,
}, },
}; };
type TestingScenario = keyof typeof testingScenariosMap; type TestingScenario = keyof typeof testingScenariosMap;
export class TaxJarOrderCreatedMockGenerator { export class TaxJarOrderConfirmedMockGenerator {
constructor(private scenario: TestingScenario = "default") {} constructor(private scenario: TestingScenario = "default") {}
generateOrder = (overrides: Partial<Order> = {}): Order => generateOrder = (overrides: Partial<Order> = {}): Order =>
structuredClone({ structuredClone({

View file

@ -1,12 +1,12 @@
import { OrderCreatedSubscriptionFragment, OrderLineFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository"; import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { TaxJarOrderCreatedPayloadLinesTransformer } from "./taxjar-order-created-payload-lines-transformer"; import { TaxJarOrderConfirmedPayloadLinesTransformer } from "./taxjar-order-confirmed-payload-lines-transformer";
const transformer = new TaxJarOrderCreatedPayloadLinesTransformer(); const transformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
const mockedLines: OrderCreatedSubscriptionFragment["lines"] = [ const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [
{ {
productSku: "sku", productSku: "sku",
productName: "Test product", productName: "Test product",
@ -67,7 +67,7 @@ const matches: TaxJarTaxCodeMatches = [
}, },
]; ];
describe("TaxJarOrderCreatedPayloadLinesTransformer", () => { describe("TaxJarOrderConfirmedPayloadLinesTransformer", () => {
it("should map payload lines correctly", () => { it("should map payload lines correctly", () => {
expect(transformer.transform(mockedLines, matches)).toEqual([ expect(transformer.transform(mockedLines, matches)).toEqual([
{ {

View file

@ -1,15 +1,15 @@
import { LineItem } from "taxjar/dist/util/types"; import { LineItem } from "taxjar/dist/util/types";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository"; import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
import { TaxJarOrderCreatedTaxCodeMatcher } from "./taxjar-order-created-tax-code-matcher"; import { TaxJarOrderConfirmedTaxCodeMatcher } from "./taxjar-order-confirmed-tax-code-matcher";
export class TaxJarOrderCreatedPayloadLinesTransformer { export class TaxJarOrderConfirmedPayloadLinesTransformer {
transform( transform(
lines: OrderCreatedSubscriptionFragment["lines"], lines: OrderConfirmedSubscriptionFragment["lines"],
matches: TaxJarTaxCodeMatches matches: TaxJarTaxCodeMatches
): LineItem[] { ): LineItem[] {
return lines.map((line) => { return lines.map((line) => {
const matcher = new TaxJarOrderCreatedTaxCodeMatcher(); const matcher = new TaxJarOrderConfirmedTaxCodeMatcher();
const taxCode = matcher.match(line, matches); const taxCode = matcher.match(line, matches);
return { return {

View file

@ -1,17 +1,17 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { TaxJarOrderCreatedMockGenerator } from "./taxjar-order-created-mock-generator"; import { TaxJarOrderConfirmedMockGenerator } from "./taxjar-order-confirmed-mock-generator";
import { import {
TaxJarOrderCreatedPayloadTransformer, TaxJarOrderConfirmedPayloadTransformer,
sumPayloadLines, sumPayloadLines,
} from "./taxjar-order-created-payload-transformer"; } from "./taxjar-order-confirmed-payload-transformer";
const mockGenerator = new TaxJarOrderCreatedMockGenerator(); const mockGenerator = new TaxJarOrderConfirmedMockGenerator();
describe("TaxJarOrderCreatedPayloadTransformer", () => { describe("TaxJarOrderConfirmedPayloadTransformer", () => {
it("returns the correct order amount", () => { it("returns the correct order amount", () => {
const orderMock = mockGenerator.generateOrder(); const orderMock = mockGenerator.generateOrder();
const providerConfig = mockGenerator.generateProviderConfig(); const providerConfig = mockGenerator.generateProviderConfig();
const transformer = new TaxJarOrderCreatedPayloadTransformer(); const transformer = new TaxJarOrderConfirmedPayloadTransformer();
const transformedPayload = transformer.transform(orderMock, providerConfig, []); const transformedPayload = transformer.transform(orderMock, providerConfig, []);
expect(transformedPayload.params.amount).toBe(239.17); expect(transformedPayload.params.amount).toBe(239.17);

View file

@ -1,11 +1,11 @@
import { LineItem } from "taxjar/dist/util/types"; import { LineItem } from "taxjar/dist/util/types";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { numbers } from "../../taxes/numbers"; import { numbers } from "../../taxes/numbers";
import { taxProviderUtils } from "../../taxes/tax-provider-utils"; import { taxProviderUtils } from "../../taxes/tax-provider-utils";
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository"; import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
import { TaxJarConfig } from "../taxjar-connection-schema"; import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarOrderCreatedTarget } from "./taxjar-order-created-adapter"; import { TaxJarOrderConfirmedTarget } from "./taxjar-order-confirmed-adapter";
import { TaxJarOrderCreatedPayloadLinesTransformer } from "./taxjar-order-created-payload-lines-transformer"; import { TaxJarOrderConfirmedPayloadLinesTransformer } from "./taxjar-order-confirmed-payload-lines-transformer";
export function sumPayloadLines(lines: LineItem[]): number { export function sumPayloadLines(lines: LineItem[]): number {
return numbers.roundFloatToTwoDecimals( return numbers.roundFloatToTwoDecimals(
@ -25,13 +25,13 @@ export function sumPayloadLines(lines: LineItem[]): number {
); );
} }
export class TaxJarOrderCreatedPayloadTransformer { export class TaxJarOrderConfirmedPayloadTransformer {
transform( transform(
order: OrderCreatedSubscriptionFragment, order: OrderConfirmedSubscriptionFragment,
taxJarConfig: TaxJarConfig, taxJarConfig: TaxJarConfig,
matches: TaxJarTaxCodeMatches matches: TaxJarTaxCodeMatches
): TaxJarOrderCreatedTarget { ): TaxJarOrderConfirmedTarget {
const linesTransformer = new TaxJarOrderCreatedPayloadLinesTransformer(); const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
const lineItems = linesTransformer.transform(order.lines, matches); const lineItems = linesTransformer.transform(order.lines, matches);
const lineSum = sumPayloadLines(lineItems); const lineSum = sumPayloadLines(lineItems);
const shippingAmount = order.shippingPrice.gross.amount; const shippingAmount = order.shippingPrice.gross.amount;

View file

@ -1,11 +1,11 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { TaxJarTaxCodeMatchesService } from "../tax-code/taxjar-tax-code-matches.service"; import { TaxJarTaxCodeMatchesService } from "../tax-code/taxjar-tax-code-matches.service";
import { TaxJarConfig } from "../taxjar-connection-schema"; import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarOrderCreatedPayloadTransformer } from "./taxjar-order-created-payload-transformer"; import { TaxJarOrderConfirmedPayloadTransformer } from "./taxjar-order-confirmed-payload-transformer";
import { CreateOrderArgs } from "../taxjar-client"; import { CreateOrderArgs } from "../taxjar-client";
export class TaxJarOrderCreatedPayloadService { export class TaxJarOrderConfirmedPayloadService {
constructor(private authData: AuthData) {} constructor(private authData: AuthData) {}
private getMatches() { private getMatches() {
@ -15,11 +15,11 @@ export class TaxJarOrderCreatedPayloadService {
} }
async getPayload( async getPayload(
order: OrderCreatedSubscriptionFragment, order: OrderConfirmedSubscriptionFragment,
taxJarConfig: TaxJarConfig taxJarConfig: TaxJarConfig
): Promise<CreateOrderArgs> { ): Promise<CreateOrderArgs> {
const matches = await this.getMatches(); const matches = await this.getMatches();
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer(); const payloadTransformer = new TaxJarOrderConfirmedPayloadTransformer();
return payloadTransformer.transform(order, taxJarConfig, matches); return payloadTransformer.transform(order, taxJarConfig, matches);
} }

View file

@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { TaxJarOrderConfirmedMockGenerator } from "./taxjar-order-confirmed-mock-generator";
import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
describe("TaxJarOrderConfirmedResponseTransformer", () => {
it("returns orded id in response", () => {
const mockGenerator = new TaxJarOrderConfirmedMockGenerator();
const responseMock = mockGenerator.generateResponse();
const transformer = new TaxJarOrderConfirmedResponseTransformer();
const result = transformer.transform(responseMock);
expect(result).toEqual({
id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2",
});
});
});

View file

@ -1,7 +1,7 @@
import { CreateOrderRes } from "taxjar/dist/types/returnTypes"; import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
export class TaxJarOrderCreatedResponseTransformer { export class TaxJarOrderConfirmedResponseTransformer {
transform(response: CreateOrderRes): CreateOrderResponse { transform(response: CreateOrderRes): CreateOrderResponse {
return { return {
id: response.order.transaction_id, id: response.order.transaction_id,

View file

@ -1,6 +1,6 @@
import { OrderLineFragment } from "../../../../generated/graphql"; import { OrderLineFragment } from "../../../../generated/graphql";
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository"; import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
import { TaxJarOrderCreatedTaxCodeMatcher } from "./taxjar-order-created-tax-code-matcher"; import { TaxJarOrderConfirmedTaxCodeMatcher } from "./taxjar-order-confirmed-tax-code-matcher";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const mockedLine: OrderLineFragment = { const mockedLine: OrderLineFragment = {
@ -42,15 +42,15 @@ const matches: TaxJarTaxCodeMatches = [
}, },
]; ];
describe("TaxJarOrderCreatedTaxCodeMatcher", () => { describe("TaxJarOrderConfirmedTaxCodeMatcher", () => {
it("should return empty string if tax class is not found", () => { it("should return empty string if tax class is not found", () => {
const matcher = new TaxJarOrderCreatedTaxCodeMatcher(); const matcher = new TaxJarOrderConfirmedTaxCodeMatcher();
expect(matcher.match(mockedLine, matches)).toEqual(""); expect(matcher.match(mockedLine, matches)).toEqual("");
}); });
it("should return tax code if tax class is found", () => { it("should return tax code if tax class is found", () => {
const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } }); const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } });
const matcher = new TaxJarOrderCreatedTaxCodeMatcher(); const matcher = new TaxJarOrderConfirmedTaxCodeMatcher();
expect(matcher.match(line, matches)).toEqual("P0000000"); expect(matcher.match(line, matches)).toEqual("P0000000");
}); });

View file

@ -1,7 +1,7 @@
import { OrderLineFragment } from "../../../../generated/graphql"; import { OrderLineFragment } from "../../../../generated/graphql";
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository"; import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
export class TaxJarOrderCreatedTaxCodeMatcher { export class TaxJarOrderConfirmedTaxCodeMatcher {
private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) { private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) {
return matches.find((m) => m.data.saleorTaxClassId === taxClassId); return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
} }

View file

@ -1,3 +0,0 @@
import { describe } from "vitest";
describe.todo("TaxJarOrderCreatedAdapter", () => {});

View file

@ -1,16 +0,0 @@
import { describe, expect, it } from "vitest";
import { TaxJarOrderCreatedMockGenerator } from "./taxjar-order-created-mock-generator";
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer";
describe("TaxJarOrderCreatedResponseTransformer", () => {
it("returns orded id in response", () => {
const mockGenerator = new TaxJarOrderCreatedMockGenerator();
const responseMock = mockGenerator.generateResponse();
const transformer = new TaxJarOrderCreatedResponseTransformer();
const result = transformer.transform(responseMock);
expect(result).toEqual({
id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2",
});
});
});

View file

@ -1,10 +1,14 @@
import { OrderCreatedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql"; import {
OrderCancelledEventSubscriptionFragment,
OrderConfirmedSubscriptionFragment,
TaxBaseFragment,
} from "../../../generated/graphql";
import { Logger, createLogger } from "../../lib/logger"; import { Logger, createLogger } from "../../lib/logger";
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter"; import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
import { TaxJarClient } from "./taxjar-client"; import { TaxJarClient } from "./taxjar-client";
import { TaxJarConfig } from "./taxjar-connection-schema"; import { TaxJarConfig } from "./taxjar-connection-schema";
import { TaxJarOrderCreatedAdapter } from "./order-created/taxjar-order-created-adapter"; import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
import { ProviderWebhookService } from "../taxes/tax-provider-webhook"; import { CreateOrderResponse, ProviderWebhookService } from "../taxes/tax-provider-webhook";
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
export class TaxJarWebhookService implements ProviderWebhookService { export class TaxJarWebhookService implements ProviderWebhookService {
@ -30,8 +34,8 @@ export class TaxJarWebhookService implements ProviderWebhookService {
return response; return response;
} }
async createOrder(order: OrderCreatedSubscriptionFragment) { async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
const adapter = new TaxJarOrderCreatedAdapter(this.config, this.authData); const adapter = new TaxJarOrderConfirmedAdapter(this.config, this.authData);
const response = await adapter.send({ order }); const response = await adapter.send({ order });
@ -42,4 +46,8 @@ export class TaxJarWebhookService implements ProviderWebhookService {
async fulfillOrder() { async fulfillOrder() {
return { ok: true }; return { ok: true };
} }
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
// TaxJar isn't implemented yet
}
} }

View file

@ -4,9 +4,9 @@ import { AppManifest } from "@saleor/app-sdk/types";
import packageJson from "../../../package.json"; 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 { orderCreatedAsyncWebhook } from "./webhooks/order-created"; 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";
export default createManifestHandler({ export default createManifestHandler({
async manifestFactory({ appBaseUrl }) { async manifestFactory({ appBaseUrl }) {
@ -35,8 +35,8 @@ export default createManifestHandler({
webhooks: [ webhooks: [
orderCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL), orderCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL), checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
orderCreatedAsyncWebhook.getWebhookManifest(apiBaseURL), orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
orderFulfilledAsyncWebhook.getWebhookManifest(apiBaseURL), orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
], ],
}; };

View file

@ -46,12 +46,7 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res,
try { try {
verifyCalculateTaxesPayload(payload); verifyCalculateTaxesPayload(payload);
logger.debug("Payload validated succesfully"); logger.debug("Payload validated succesfully");
} catch (error) {
logger.debug("Payload validation failed");
return webhookResponse.error(error);
}
try {
const appMetadata = payload.recipient?.privateMetadata ?? []; const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.taxBase.channel.slug; const channelSlug = payload.taxBase.channel.slug;
const activeConnectionService = getActiveConnectionService( const activeConnectionService = getActiveConnectionService(

View file

@ -46,12 +46,7 @@ export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx
try { try {
verifyCalculateTaxesPayload(payload); verifyCalculateTaxesPayload(payload);
logger.debug("Payload validated succesfully"); logger.debug("Payload validated succesfully");
} catch (error) {
logger.debug("Payload validation failed");
return webhookResponse.error(error);
}
try {
const appMetadata = payload.recipient?.privateMetadata ?? []; const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.taxBase.channel.slug; const channelSlug = payload.taxBase.channel.slug;
const activeConnectionService = getActiveConnectionService( const activeConnectionService = getActiveConnectionService(

View file

@ -1,7 +1,7 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { import {
OrderFulfilledEventSubscriptionFragment, OrderCancelledEventSubscriptionFragment,
UntypedOrderFulfilledSubscriptionDocument, UntypedOrderCancelledSubscriptionDocument,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app"; import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger"; import { createLogger } from "../../../lib/logger";
@ -13,43 +13,43 @@ export const config = {
}, },
}; };
type OrderFulfilledPayload = Extract< export type OrderCancelledPayload = Extract<
OrderFulfilledEventSubscriptionFragment, OrderCancelledEventSubscriptionFragment,
{ __typename: "OrderFulfilled" } { __typename: "OrderCancelled" }
>; >;
export const orderFulfilledAsyncWebhook = new SaleorAsyncWebhook<OrderFulfilledPayload>({ export const orderCancelledAsyncWebhook = new SaleorAsyncWebhook<OrderCancelledPayload>({
name: "OrderFulfilled", name: "OrderCancelled",
apl: saleorApp.apl, apl: saleorApp.apl,
event: "ORDER_FULFILLED", event: "ORDER_CANCELLED",
query: UntypedOrderFulfilledSubscriptionDocument, query: UntypedOrderCancelledSubscriptionDocument,
webhookPath: "/api/webhooks/order-fulfilled", webhookPath: "/api/webhooks/order-cancelled",
}); });
export default orderFulfilledAsyncWebhook.createHandler(async (req, res, ctx) => { export default orderCancelledAsyncWebhook.createHandler(async (req, res, ctx) => {
const logger = createLogger({ event: ctx.event }); const logger = createLogger({ event: ctx.event });
const { payload } = ctx; const { payload } = ctx;
const webhookResponse = new WebhookResponse(res); const webhookResponse = new WebhookResponse(res);
logger.info("Handler called with payload"); logger.info("Handler called with payload");
if (!payload.order) {
return webhookResponse.error(new Error("Insufficient order data"));
}
try { try {
const appMetadata = payload.recipient?.privateMetadata ?? []; const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.order?.channel.slug; const channelSlug = payload.order.channel.slug;
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData); const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
logger.info("Fetched taxProvider"); logger.info("Cancelling order...");
// todo: figure out what fields are needed and add validation await taxProvider.cancelOrder(payload);
if (!payload.order) {
return webhookResponse.error(new Error("Insufficient order data"));
}
await taxProvider.fulfillOrder(payload.order);
logger.info("Order fulfilled"); logger.info("Order cancelled");
return webhookResponse.success(); return webhookResponse.success();
} catch (error) { } catch (error) {
return webhookResponse.error(new Error("Error while fulfilling tax provider order")); return webhookResponse.error(new Error("Error while cancelling tax provider order"));
} }
}); });

View file

@ -1,19 +1,15 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { import {
OrderCreatedEventSubscriptionFragment, OrderConfirmedEventSubscriptionFragment,
OrderStatus, OrderStatus,
UntypedOrderCreatedSubscriptionDocument, UntypedOrderConfirmedSubscriptionDocument,
UpdateMetadataDocument,
UpdateMetadataMutation,
UpdateMetadataMutationVariables,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app"; import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger"; import { createLogger } from "../../../lib/logger";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service"; import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { Client } from "urql";
import { WebhookResponse } from "../../../modules/app/webhook-response"; import { WebhookResponse } from "../../../modules/app/webhook-response";
import { PROVIDER_ORDER_ID_KEY } from "../../../modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { OrderMetadataManager } from "../../../modules/app/order-metadata-manager";
export const config = { export const config = {
api: { api: {
@ -21,49 +17,20 @@ export const config = {
}, },
}; };
type OrderCreatedPayload = Extract< type OrderConfirmedPayload = Extract<
OrderCreatedEventSubscriptionFragment, OrderConfirmedEventSubscriptionFragment,
{ __typename: "OrderCreated" } { __typename: "OrderConfirmed" }
>; >;
export const orderCreatedAsyncWebhook = new SaleorAsyncWebhook<OrderCreatedPayload>({ export const orderConfirmedAsyncWebhook = new SaleorAsyncWebhook<OrderConfirmedPayload>({
name: "OrderCreated", name: "OrderConfirmed",
apl: saleorApp.apl, apl: saleorApp.apl,
event: "ORDER_CREATED", event: "ORDER_CONFIRMED",
query: UntypedOrderCreatedSubscriptionDocument, query: UntypedOrderConfirmedSubscriptionDocument,
webhookPath: "/api/webhooks/order-created", webhookPath: "/api/webhooks/order-confirmed",
}); });
/** export default orderConfirmedAsyncWebhook.createHandler(async (req, res, ctx) => {
* We need to store the provider order id in the Saleor order metadata so that we can
* update the provider order when the Saleor order is fulfilled.
*/
async function updateOrderMetadataWithExternalId(
client: Client,
orderId: string,
externalId: string
) {
const variables: UpdateMetadataMutationVariables = {
id: orderId,
input: [
{
key: PROVIDER_ORDER_ID_KEY,
value: externalId,
},
],
};
const { error } = await client
.mutation<UpdateMetadataMutation>(UpdateMetadataDocument, variables)
.toPromise();
if (error) {
throw error;
}
return { ok: true };
}
export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => {
const logger = createLogger({ event: ctx.event }); const logger = createLogger({ event: ctx.event });
const { payload, authData } = ctx; const { payload, authData } = ctx;
const { saleorApiUrl, token } = authData; const { saleorApiUrl, token } = authData;
@ -76,8 +43,6 @@ export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => {
const channelSlug = payload.order?.channel.slug; const channelSlug = payload.order?.channel.slug;
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData); const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
logger.info("Fetched taxProvider");
// todo: figure out what fields are needed and add validation // todo: figure out what fields are needed and add validation
if (!payload.order) { if (!payload.order) {
return webhookResponse.error(new Error("Insufficient order data")); return webhookResponse.error(new Error("Insufficient order data"));
@ -87,15 +52,22 @@ export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => {
return webhookResponse.error(new Error("Skipping fulfilled order to prevent duplication")); return webhookResponse.error(new Error("Skipping fulfilled order to prevent duplication"));
} }
const createdOrder = await taxProvider.createOrder(payload.order); logger.info("Creating order...");
logger.info({ createdOrder }, "Order created"); const confirmedOrder = await taxProvider.confirmOrder(payload.order);
logger.info({ confirmedOrder }, "Order confirmed");
const client = createGraphQLClient({ const client = createGraphQLClient({
saleorApiUrl, saleorApiUrl,
token, token,
}); });
await updateOrderMetadataWithExternalId(client, payload.order.id, createdOrder.id); const orderMetadataManager = new OrderMetadataManager(client);
await orderMetadataManager.updateOrderMetadataWithExternalId(
payload.order.id,
confirmedOrder.id
);
logger.info("Updated order metadata with externalId"); logger.info("Updated order metadata with externalId");
return webhookResponse.success(); return webhookResponse.success();

View file

@ -1337,8 +1337,8 @@ importers:
specifier: ^2.1.4 specifier: ^2.1.4
version: 2.1.4(graphql@16.7.1) version: 2.1.4(graphql@16.7.1)
avatax: avatax:
specifier: ^23.3.2 specifier: ^23.7.0
version: 23.3.2 version: 23.7.0
clsx: clsx:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@ -11256,8 +11256,8 @@ packages:
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
/avatax@23.3.2: /avatax@23.7.0:
resolution: {integrity: sha512-WGjSIQNvdM0XSyukjb+QIxB1z4xaEOdaGmPhXwLJYgxIuJlDw7Uq1nHUHo+yeHKZk3dKGzCyrFaa8N1Wx9nSbQ==} resolution: {integrity: sha512-34CuafkIgzfj6B8gTMmeUN5iYWdgHtSKRwIRWIYFcP/OpUMDOGB0WMMdGrZUgthLjYCWOkQdi2QReWuM/TSROg==}
dependencies: dependencies:
'@types/node-fetch': 2.6.4 '@types/node-fetch': 2.6.4
form-data: 4.0.0 form-data: 4.0.0