Compare commits
21 commits
main
...
taxes/docu
Author | SHA1 | Date | |
---|---|---|---|
![]() |
030e373eb9 | ||
![]() |
b2f6226648 | ||
![]() |
25696d6a98 | ||
![]() |
6106e1d5fa | ||
![]() |
2b073e7113 | ||
![]() |
09c3be9ad2 | ||
![]() |
c3cc7a8b5e | ||
![]() |
32a7a91294 | ||
![]() |
7cb991abaf | ||
![]() |
293407e9f0 | ||
![]() |
79a1411a55 | ||
![]() |
547e773500 | ||
![]() |
fb8aa45f4c | ||
![]() |
d2a9686937 | ||
![]() |
e4ac0e7827 | ||
![]() |
ac9acdf0c6 | ||
![]() |
d458452573 | ||
![]() |
0013af2f6d | ||
![]() |
b167fe1b4a | ||
![]() |
146517062a | ||
![]() |
1a41ca7684 |
67 changed files with 595 additions and 566 deletions
5
.changeset/cold-toes-heal.md
Normal file
5
.changeset/cold-toes-heal.md
Normal 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.
|
5
.changeset/forty-shirts-battle.md
Normal file
5
.changeset/forty-shirts-battle.md
Normal 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.
|
5
.changeset/ninety-lobsters-design.md
Normal file
5
.changeset/ninety-lobsters-design.md
Normal 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").
|
5
.changeset/shiny-meals-wait.md
Normal file
5
.changeset/shiny-meals-wait.md
Normal 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.
|
5
.changeset/soft-steaks-know.md
Normal file
5
.changeset/soft-steaks-know.md
Normal 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.
|
|
@ -61,13 +61,13 @@ fragment TaxBase on TaxableObject {
|
|||
}
|
||||
sourceObject {
|
||||
... on Checkout {
|
||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||
avataxEntityCode: privateMetafield(key: "avataxEntityCode")
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on Order {
|
||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||
avataxEntityCode: privateMetafield(key: "avataxEntityCode")
|
||||
user {
|
||||
id
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mutation UpdateMetadata($id: ID!, $input: [MetadataInput!]!) {
|
||||
mutation UpdatePrivateMetadata($id: ID!, $input: [MetadataInput!]!) {
|
||||
updatePrivateMetadata(id: $id, input: $input) {
|
||||
item {
|
||||
privateMetadata {
|
10
apps/taxes/graphql/mutations/UpdatePublicMetadata.graphql
Normal file
10
apps/taxes/graphql/mutations/UpdatePublicMetadata.graphql
Normal file
|
@ -0,0 +1,10 @@
|
|||
mutation UpdatePublicMetadata($id: ID!, $input: [MetadataInput!]!) {
|
||||
updateMetadata(id: $id, input: $input) {
|
||||
item {
|
||||
metadata {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
apps/taxes/graphql/subscriptions/OrderCancelled.graphql
Normal file
29
apps/taxes/graphql/subscriptions/OrderCancelled.graphql
Normal 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
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ fragment OrderLine on OrderLine {
|
|||
}
|
||||
}
|
||||
|
||||
fragment OrderCreatedSubscription on Order {
|
||||
fragment OrderConfirmedSubscription on Order {
|
||||
id
|
||||
user {
|
||||
id
|
||||
|
@ -64,13 +64,15 @@ fragment OrderCreatedSubscription on Order {
|
|||
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
|
||||
... on OrderCreated {
|
||||
... on OrderConfirmed {
|
||||
order {
|
||||
...OrderCreatedSubscription
|
||||
...OrderConfirmedSubscription
|
||||
}
|
||||
}
|
||||
recipient {
|
||||
|
@ -80,8 +82,8 @@ fragment OrderCreatedEventSubscription on Event {
|
|||
}
|
||||
}
|
||||
}
|
||||
subscription OrderCreatedSubscription {
|
||||
subscription OrderConfirmedSubscription {
|
||||
event {
|
||||
...OrderCreatedEventSubscription
|
||||
...OrderConfirmedEventSubscription
|
||||
}
|
||||
}
|
|
@ -47,10 +47,7 @@ fragment OrderFulfilledSubscription on Order {
|
|||
amount
|
||||
}
|
||||
}
|
||||
privateMetadata {
|
||||
key
|
||||
value
|
||||
}
|
||||
avataxId: metafield(key: "avataxId")
|
||||
lines {
|
||||
...OrderLine
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"@trpc/react-query": "10.34.0",
|
||||
"@trpc/server": "10.34.0",
|
||||
"@urql/exchange-auth": "^2.1.4",
|
||||
"avatax": "^23.3.2",
|
||||
"avatax": "^23.7.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"graphql": "16.7.1",
|
||||
|
|
|
@ -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",
|
||||
user: {
|
||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Client, gql } from "urql";
|
|||
import {
|
||||
FetchAppDetailsDocument,
|
||||
FetchAppDetailsQuery,
|
||||
UpdateMetadataDocument,
|
||||
UpdatePrivateMetadataDocument,
|
||||
} from "../../../generated/graphql";
|
||||
|
||||
gql`
|
||||
|
@ -45,7 +45,7 @@ export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]>
|
|||
|
||||
export async function mutateMetadata(client: Client, metadata: MetadataEntry[], appId: string) {
|
||||
const { error: mutationError, data: mutationData } = await client
|
||||
.mutation(UpdateMetadataDocument, {
|
||||
.mutation(UpdatePrivateMetadataDocument, {
|
||||
id: appId,
|
||||
input: metadata,
|
||||
})
|
||||
|
|
39
apps/taxes/src/modules/app/order-metadata-manager.ts
Normal file
39
apps/taxes/src/modules/app/order-metadata-manager.ts
Normal 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 };
|
||||
}
|
||||
}
|
|
@ -1,36 +1,58 @@
|
|||
import { NextApiResponse } from "next";
|
||||
|
||||
import { AvalaraError } from "avatax/lib/AvaTaxClient";
|
||||
import { ZodError } from "zod";
|
||||
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 {
|
||||
private logger: Logger;
|
||||
constructor(private res: NextApiResponse) {
|
||||
this.logger = createLogger({ event: "WebhookResponse" });
|
||||
}
|
||||
|
||||
private returnSuccess(data?: unknown) {
|
||||
this.logger.debug({ data }, "Responding to Saleor with data:");
|
||||
return this.res.status(200).json(data ?? {});
|
||||
}
|
||||
error(error: unknown) {
|
||||
const errorResolver = new WebhookErrorResolver();
|
||||
const errorMessage = errorResolver.resolve(error);
|
||||
|
||||
private returnError(errorMessage: string) {
|
||||
this.logger.debug({ errorMessage }, "Responding to Saleor with error:");
|
||||
|
||||
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) {
|
||||
return this.returnSuccess(data);
|
||||
return this.res.status(200).json(data ?? {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@ import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|||
import { AddressLocationInfo as AvataxAddress } from "avatax/lib/models/AddressLocationInfo";
|
||||
import { CommitTransactionModel } from "avatax/lib/models/CommitTransactionModel";
|
||||
import { CreateTransactionModel } from "avatax/lib/models/CreateTransactionModel";
|
||||
import { LogOptions } from "avatax/lib/utils/logger";
|
||||
import packageJson from "../../../package.json";
|
||||
import { createLogger, Logger } from "../../lib/logger";
|
||||
import { AvataxClientTaxCodeService } from "./avatax-client-tax-code.service";
|
||||
import { BaseAvataxConfig } from "./avatax-connection-schema";
|
||||
import { VoidReasonCode } from "avatax/lib/enums/VoidReasonCode";
|
||||
|
||||
type AvataxSettings = {
|
||||
appName: string;
|
||||
|
@ -14,11 +15,7 @@ type AvataxSettings = {
|
|||
environment: "sandbox" | "production";
|
||||
machineName: string;
|
||||
timeout: number;
|
||||
logOptions?: {
|
||||
logEnabled: boolean;
|
||||
logLevel: number;
|
||||
logRequestAndResponseInfo: boolean;
|
||||
};
|
||||
logOptions?: LogOptions;
|
||||
};
|
||||
|
||||
const defaultAvataxSettings: AvataxSettings = {
|
||||
|
@ -53,12 +50,15 @@ export type ValidateAddressArgs = {
|
|||
address: AvataxAddress;
|
||||
};
|
||||
|
||||
export type VoidTransactionArgs = {
|
||||
transactionCode: string;
|
||||
companyCode: string;
|
||||
};
|
||||
|
||||
export class AvataxClient {
|
||||
private client: Avatax;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(baseConfig: BaseAvataxConfig) {
|
||||
this.logger = createLogger({ name: "AvataxClient" });
|
||||
const settings = createAvataxSettings({ isSandbox: baseConfig.isSandbox });
|
||||
const avataxClient = new Avatax(settings).withSecurity(baseConfig.credentials);
|
||||
|
||||
|
@ -73,6 +73,20 @@ export class AvataxClient {
|
|||
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) {
|
||||
return this.client.resolveAddress(address);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export type BaseAvataxConfig = z.infer<typeof baseAvataxConfigSchema>;
|
|||
export const avataxConfigSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, { message: "Name requires at least one character." }),
|
||||
companyCode: z.string().optional(),
|
||||
companyCode: z.string(),
|
||||
isAutocommit: z.boolean(),
|
||||
shippingTaxCode: z.string().optional(),
|
||||
isDocumentRecordingEnabled: z.boolean().default(true),
|
||||
|
@ -36,7 +36,7 @@ export type AvataxConfig = z.infer<typeof avataxConfigSchema>;
|
|||
|
||||
export const defaultAvataxConfig: AvataxConfig = {
|
||||
name: "",
|
||||
companyCode: "",
|
||||
companyCode: "DEFAULT",
|
||||
isSandbox: false,
|
||||
isAutocommit: false,
|
||||
isDocumentRecordingEnabled: true,
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import {
|
||||
OrderCreatedSubscriptionFragment,
|
||||
OrderConfirmedSubscriptionFragment,
|
||||
OrderFulfilledSubscriptionFragment,
|
||||
TaxBaseFragment,
|
||||
} from "../../../generated/graphql";
|
||||
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 { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
|
||||
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
||||
import { AvataxOrderCreatedAdapter } from "./order-created/avatax-order-created-adapter";
|
||||
import { AvataxOrderFulfilledAdapter } from "./order-fulfilled/avatax-order-fulfilled-adapter";
|
||||
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
||||
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
||||
|
||||
export class AvataxWebhookService implements ProviderWebhookService {
|
||||
config = defaultAvataxConfig;
|
||||
|
@ -35,19 +36,17 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
|||
return response;
|
||||
}
|
||||
|
||||
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||
const adapter = new AvataxOrderCreatedAdapter(this.config, this.authData);
|
||||
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
|
||||
const adapter = new AvataxOrderConfirmedAdapter(this.config, this.authData);
|
||||
|
||||
const response = await adapter.send({ order });
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fulfillOrder(order: OrderFulfilledSubscriptionFragment) {
|
||||
const adapter = new AvataxOrderFulfilledAdapter(this.config);
|
||||
async cancelOrder(payload: OrderCancelledPayload) {
|
||||
const adapter = new AvataxOrderCancelledAdapter(this.config);
|
||||
|
||||
const response = await adapter.send({ order });
|
||||
|
||||
return response;
|
||||
await adapter.send(payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
|
@ -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" });
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it } from "vitest";
|
||||
|
||||
describe("AvataxOrderCreatedAdapter", () => {
|
||||
describe("AvataxOrderConfirmedAdapter", () => {
|
||||
// ? how to mock internal call to avatax?
|
||||
it.todo("calls avatax client", () => {});
|
||||
});
|
|
@ -1,32 +1,31 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../../lib/logger";
|
||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||
import { AvataxClient } from "../avatax-client";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
||||
import { AvataxOrderCreatedPayloadService } from "./avatax-order-created-payload.service";
|
||||
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer";
|
||||
import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service";
|
||||
import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
|
||||
|
||||
type AvataxOrderCreatedPayload = {
|
||||
order: OrderCreatedSubscriptionFragment;
|
||||
type AvataxOrderConfirmedPayload = {
|
||||
order: OrderConfirmedSubscriptionFragment;
|
||||
};
|
||||
type AvataxOrderCreatedResponse = CreateOrderResponse;
|
||||
type AvataxOrderConfirmedResponse = CreateOrderResponse;
|
||||
|
||||
export class AvataxOrderCreatedAdapter
|
||||
implements WebhookAdapter<AvataxOrderCreatedPayload, AvataxOrderCreatedResponse>
|
||||
export class AvataxOrderConfirmedAdapter
|
||||
implements WebhookAdapter<AvataxOrderConfirmedPayload, AvataxOrderConfirmedResponse>
|
||||
{
|
||||
private logger: Logger;
|
||||
|
||||
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...");
|
||||
|
||||
const payloadService = new AvataxOrderCreatedPayloadService(this.authData);
|
||||
const payloadService = new AvataxOrderConfirmedPayloadService(this.authData);
|
||||
const target = await payloadService.getPayload(payload.order, this.config);
|
||||
|
||||
this.logger.debug("Calling Avatax createTransaction with transformed payload...");
|
||||
|
@ -36,7 +35,7 @@ export class AvataxOrderCreatedAdapter
|
|||
|
||||
this.logger.debug("Avatax createTransaction successfully responded");
|
||||
|
||||
const responseTransformer = new AvataxOrderCreatedResponseTransformer();
|
||||
const responseTransformer = new AvataxOrderConfirmedResponseTransformer();
|
||||
const transformedResponse = responseTransformer.transform(response);
|
||||
|
||||
this.logger.debug("Transformed Avatax createTransaction response");
|
|
@ -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"));
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
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 { orderCreatedTransactionMock } from "./avatax-order-created-response-transaction-mock";
|
||||
import { orderConfirmedTransactionMock } from "./avatax-order-confirmed-response-transaction-mock";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
import { defaultOrder } from "../../../mocks";
|
||||
import { AvataxConfigMockGenerator } from "../avatax-config-mock-generator";
|
||||
|
@ -14,23 +14,23 @@ const defaultChannelConfig: ChannelConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const defaultOrderCreatedResponse: TransactionModel = orderCreatedTransactionMock;
|
||||
const defaultOrderConfirmedResponse: TransactionModel = orderConfirmedTransactionMock;
|
||||
|
||||
const testingScenariosMap = {
|
||||
default: {
|
||||
order: defaultOrder,
|
||||
channelConfig: defaultChannelConfig,
|
||||
response: defaultOrderCreatedResponse,
|
||||
response: defaultOrderConfirmedResponse,
|
||||
},
|
||||
};
|
||||
|
||||
type TestingScenario = keyof typeof testingScenariosMap;
|
||||
|
||||
export class AvataxOrderCreatedMockGenerator {
|
||||
export class AvataxOrderConfirmedMockGenerator {
|
||||
constructor(private scenario: TestingScenario = "default") {}
|
||||
generateOrder = (
|
||||
overrides: Partial<OrderCreatedSubscriptionFragment> = {}
|
||||
): OrderCreatedSubscriptionFragment =>
|
||||
overrides: Partial<OrderConfirmedSubscriptionFragment> = {}
|
||||
): OrderConfirmedSubscriptionFragment =>
|
||||
structuredClone({
|
||||
...testingScenariosMap[this.scenario].order,
|
||||
...overrides,
|
|
@ -1,16 +1,16 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer";
|
||||
import { avataxConfigMock } from "./avatax-order-created-payload-transformer.test";
|
||||
import { AvataxOrderConfirmedPayloadLinesTransformer } from "./avatax-order-confirmed-payload-lines-transformer";
|
||||
import { avataxConfigMock } from "./avatax-order-confirmed-payload-transformer.test";
|
||||
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 mockGenerator = new AvataxOrderCreatedMockGenerator();
|
||||
const linesTransformer = new AvataxOrderConfirmedPayloadLinesTransformer();
|
||||
const mockGenerator = new AvataxOrderConfirmedMockGenerator();
|
||||
const orderMock = mockGenerator.generateOrder();
|
||||
|
||||
const matches: AvataxTaxCodeMatches = [];
|
||||
|
||||
describe("AvataxOrderCreatedPayloadLinesTransformer", () => {
|
||||
describe("AvataxOrderConfirmedPayloadLinesTransformer", () => {
|
||||
const lines = linesTransformer.transform(orderMock, avataxConfigMock, matches);
|
||||
|
||||
it("returns the correct number of lines", () => {
|
|
@ -1,19 +1,19 @@
|
|||
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { numbers } from "../../taxes/numbers";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||
import { SHIPPING_ITEM_CODE } from "./avatax-order-created-payload-transformer";
|
||||
import { AvataxOrderCreatedTaxCodeMatcher } from "./avatax-order-created-tax-code-matcher";
|
||||
import { SHIPPING_ITEM_CODE } from "./avatax-order-confirmed-payload-transformer";
|
||||
import { AvataxOrderConfirmedTaxCodeMatcher } from "./avatax-order-confirmed-tax-code-matcher";
|
||||
|
||||
export class AvataxOrderCreatedPayloadLinesTransformer {
|
||||
export class AvataxOrderConfirmedPayloadLinesTransformer {
|
||||
transform(
|
||||
order: OrderCreatedSubscriptionFragment,
|
||||
order: OrderConfirmedSubscriptionFragment,
|
||||
config: AvataxConfig,
|
||||
matches: AvataxTaxCodeMatches
|
||||
): LineItemModel[] {
|
||||
const productLines: LineItemModel[] = order.lines.map((line) => {
|
||||
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
||||
const matcher = new AvataxOrderConfirmedTaxCodeMatcher();
|
||||
const taxCode = matcher.match(line, matches);
|
||||
|
||||
return {
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator";
|
||||
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
||||
import { AvataxOrderConfirmedMockGenerator } from "./avatax-order-confirmed-mock-generator";
|
||||
import { AvataxOrderConfirmedPayloadTransformer } from "./avatax-order-confirmed-payload-transformer";
|
||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||
|
||||
const mockGenerator = new AvataxOrderCreatedMockGenerator();
|
||||
const mockGenerator = new AvataxOrderConfirmedMockGenerator();
|
||||
|
||||
const orderMock = 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();
|
||||
|
||||
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||
describe("AvataxOrderConfirmedPayloadTransformer", () => {
|
||||
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", async () => {
|
||||
const payload = await transformer.transform(orderMock, avataxConfigMock, []);
|
||||
|
||||
|
@ -48,7 +48,7 @@ describe("AvataxOrderCreatedPayloadTransformer", () => {
|
|||
expect(check).toBe(true);
|
||||
});
|
||||
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 linesWithoutShipping = payload.model.lines.slice(0, -1);
|
|
@ -1,16 +1,18 @@
|
|||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { discountUtils } from "../../taxes/discount-utils";
|
||||
import { avataxAddressFactory } from "../address-factory";
|
||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
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 { AvataxOrderConfirmedCalculationDateResolver } from "./avatax-order-confirmed-calculation-date-resolver";
|
||||
import { AvataxOrderConfirmedDocumentCodeResolver } from "./avatax-order-confirmed-document-code-resolver";
|
||||
|
||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||
|
||||
export class AvataxOrderCreatedPayloadTransformer {
|
||||
export class AvataxOrderConfirmedPayloadTransformer {
|
||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
||||
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.
|
||||
|
@ -19,18 +21,26 @@ export class AvataxOrderCreatedPayloadTransformer {
|
|||
|
||||
return DocumentType.SalesInvoice;
|
||||
}
|
||||
|
||||
async transform(
|
||||
order: OrderCreatedSubscriptionFragment,
|
||||
order: OrderConfirmedSubscriptionFragment,
|
||||
avataxConfig: AvataxConfig,
|
||||
matches: AvataxTaxCodeMatches
|
||||
): Promise<CreateTransactionArgs> {
|
||||
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
||||
const avataxClient = new AvataxClient(avataxConfig);
|
||||
|
||||
const linesTransformer = new AvataxOrderConfirmedPayloadLinesTransformer();
|
||||
const dateResolver = new AvataxOrderConfirmedCalculationDateResolver();
|
||||
const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient });
|
||||
const documentCodeResolver = new AvataxOrderConfirmedDocumentCodeResolver();
|
||||
|
||||
const entityUseCode = await entityTypeMatcher.match(order.avataxEntityCode);
|
||||
const date = dateResolver.resolve(order);
|
||||
const code = documentCodeResolver.resolve(order);
|
||||
|
||||
return {
|
||||
model: {
|
||||
code,
|
||||
type: this.matchDocumentType(avataxConfig),
|
||||
entityUseCode,
|
||||
customerCode:
|
||||
|
@ -47,7 +57,7 @@ export class AvataxOrderCreatedPayloadTransformer {
|
|||
currencyCode: order.total.currency,
|
||||
email: order.user?.email ?? "",
|
||||
lines: linesTransformer.transform(order, avataxConfig, matches),
|
||||
date: new Date(order.created),
|
||||
date,
|
||||
discount: discountUtils.sumDiscounts(
|
||||
order.discounts.map((discount) => discount.amount.amount)
|
||||
),
|
|
@ -1,11 +1,11 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { CreateTransactionArgs } from "../avatax-client";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
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) {}
|
||||
|
||||
private getMatches() {
|
||||
|
@ -15,11 +15,11 @@ export class AvataxOrderCreatedPayloadService {
|
|||
}
|
||||
|
||||
async getPayload(
|
||||
order: OrderCreatedSubscriptionFragment,
|
||||
order: OrderConfirmedSubscriptionFragment,
|
||||
avataxConfig: AvataxConfig
|
||||
): Promise<CreateTransactionArgs> {
|
||||
const matches = await this.getMatches();
|
||||
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer();
|
||||
const payloadTransformer = new AvataxOrderConfirmedPayloadTransformer();
|
||||
|
||||
return payloadTransformer.transform(order, avataxConfig, matches);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||
|
||||
export const orderCreatedTransactionMock = {
|
||||
export const orderConfirmedTransactionMock = {
|
||||
id: 85026453274669,
|
||||
code: "31fa9e94-e8df-429b-b303-549e7bc10f1e",
|
||||
companyId: 7799660,
|
|
@ -9,7 +9,7 @@ import { LiabilityType } from "avatax/lib/enums/LiabilityType";
|
|||
import { RateType } from "avatax/lib/enums/RateType";
|
||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||
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 = {
|
||||
id: 0,
|
||||
|
@ -497,9 +497,9 @@ const MOCKED_TRANSACTION: TransactionModel = {
|
|||
],
|
||||
};
|
||||
|
||||
describe("AvataxOrderCreatedResponseTransformer", () => {
|
||||
describe("AvataxOrderConfirmedResponseTransformer", () => {
|
||||
it("returns orded id in response", () => {
|
||||
const transformer = new AvataxOrderCreatedResponseTransformer();
|
||||
const transformer = new AvataxOrderConfirmedResponseTransformer();
|
||||
const result = transformer.transform(MOCKED_TRANSACTION);
|
||||
|
||||
expect(result).toEqual({
|
||||
|
@ -507,7 +507,7 @@ describe("AvataxOrderCreatedResponseTransformer", () => {
|
|||
});
|
||||
});
|
||||
it("throws an error when no transaction id is present", () => {
|
||||
const transformer = new AvataxOrderCreatedResponseTransformer();
|
||||
const transformer = new AvataxOrderConfirmedResponseTransformer();
|
||||
|
||||
expect(() => transformer.transform({} as any)).toThrowError();
|
||||
});
|
|
@ -2,7 +2,7 @@ import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
|||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
|
||||
export class AvataxOrderCreatedResponseTransformer {
|
||||
export class AvataxOrderConfirmedResponseTransformer {
|
||||
transform(response: TransactionModel): CreateOrderResponse {
|
||||
return {
|
||||
id: taxProviderUtils.resolveOptionalOrThrow(
|
|
@ -1,7 +1,7 @@
|
|||
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||
import { describe, expect, it } from "vitest";
|
||||
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 = {
|
||||
productSku: "sku",
|
||||
|
@ -42,15 +42,15 @@ const matches: AvataxTaxCodeMatches = [
|
|||
},
|
||||
];
|
||||
|
||||
describe("AvataxOrderCreatedTaxCodeMatcher", () => {
|
||||
describe("AvataxOrderConfirmedTaxCodeMatcher", () => {
|
||||
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("");
|
||||
});
|
||||
it("should return tax code if tax class is found", () => {
|
||||
const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } });
|
||||
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
||||
const matcher = new AvataxOrderConfirmedTaxCodeMatcher();
|
||||
|
||||
expect(matcher.match(line, matches)).toEqual("P0000000");
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||
|
||||
export class AvataxOrderCreatedTaxCodeMatcher {
|
||||
export class AvataxOrderConfirmedTaxCodeMatcher {
|
||||
private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) {
|
||||
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../../lib/logger";
|
||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||
import { AvataxClient, CommitTransactionArgs } from "../avatax-client";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
|
||||
import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer";
|
||||
|
||||
export type AvataxOrderFulfilledPayload = {
|
||||
order: OrderFulfilledSubscriptionFragment;
|
||||
};
|
||||
export type AvataxOrderFulfilledTarget = CommitTransactionArgs;
|
||||
export type AvataxOrderFulfilledResponse = { ok: true };
|
||||
|
||||
export class AvataxOrderFulfilledAdapter
|
||||
implements WebhookAdapter<AvataxOrderFulfilledPayload, AvataxOrderFulfilledResponse>
|
||||
{
|
||||
private logger: Logger;
|
||||
|
||||
constructor(private readonly config: AvataxConfig) {
|
||||
this.logger = createLogger({ name: "AvataxOrderFulfilledAdapter" });
|
||||
}
|
||||
|
||||
async send(payload: AvataxOrderFulfilledPayload): Promise<AvataxOrderFulfilledResponse> {
|
||||
this.logger.debug("Transforming the Saleor payload for commiting transaction with Avatax...");
|
||||
|
||||
const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer(this.config);
|
||||
const target = payloadTransformer.transform({ ...payload });
|
||||
|
||||
this.logger.debug("Calling Avatax commitTransaction with transformed payload...");
|
||||
|
||||
const client = new AvataxClient(this.config);
|
||||
const response = await client.commitTransaction(target);
|
||||
|
||||
this.logger.debug("Avatax commitTransaction succesfully responded");
|
||||
|
||||
const responseTransformer = new AvataxOrderFulfilledResponseTransformer();
|
||||
const transformedResponse = responseTransformer.transform(response);
|
||||
|
||||
this.logger.debug("Transformed Avatax commitTransaction response");
|
||||
|
||||
return transformedResponse;
|
||||
}
|
||||
}
|
|
@ -1,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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import {
|
||||
MetadataItem,
|
||||
OrderCreatedSubscriptionFragment,
|
||||
OrderCancelledEventSubscriptionFragment,
|
||||
OrderConfirmedSubscriptionFragment,
|
||||
OrderFulfilledSubscriptionFragment,
|
||||
TaxBaseFragment,
|
||||
} from "../../../generated/graphql";
|
||||
|
@ -11,7 +12,8 @@ import { getAppConfig } from "../app/get-app-config";
|
|||
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
||||
import { ProviderConnection } from "../provider-connections/provider-connections";
|
||||
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
|
||||
class ActiveTaxProviderService implements ProviderWebhookService {
|
||||
|
@ -48,12 +50,12 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
|||
return this.client.calculateTaxes(payload);
|
||||
}
|
||||
|
||||
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||
return this.client.createOrder(order);
|
||||
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
|
||||
return this.client.confirmOrder(order);
|
||||
}
|
||||
|
||||
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
|
||||
return this.client.fulfillOrder(payload);
|
||||
async cancelOrder(payload: OrderCancelledPayload) {
|
||||
this.client.cancelOrder(payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
* 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.
|
||||
*/
|
||||
function resolveOptionalOrThrow<T>(value: T | undefined, error?: Error): T {
|
||||
if (value === undefined) {
|
||||
function resolveOptionalOrThrow<T>(value: T | undefined | null, error?: Error): T {
|
||||
if (value === undefined || value === null) {
|
||||
throw error
|
||||
? error
|
||||
: new Error("Could not resolve data. Value needed for further calculation is undefined.");
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
OrderCreatedSubscriptionFragment,
|
||||
OrderConfirmedSubscriptionFragment,
|
||||
OrderFulfilledSubscriptionFragment,
|
||||
TaxBaseFragment,
|
||||
} 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"];
|
||||
|
||||
|
@ -12,6 +12,6 @@ export type CreateOrderResponse = { id: string };
|
|||
|
||||
export interface ProviderWebhookService {
|
||||
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
||||
createOrder: (payload: OrderCreatedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||
fulfillOrder: (payload: OrderFulfilledSubscriptionFragment) => Promise<{ ok: boolean }>;
|
||||
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { describe } from "vitest";
|
||||
|
||||
describe.todo("TaxJarOrderConfirmedAdapter", () => {});
|
|
@ -1,30 +1,30 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../../lib/logger";
|
||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||
import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
|
||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||
import { TaxJarOrderCreatedPayloadService } from "./taxjar-order-created-payload.service";
|
||||
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer";
|
||||
import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service";
|
||||
import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
|
||||
|
||||
export type TaxJarOrderCreatedPayload = {
|
||||
order: OrderCreatedSubscriptionFragment;
|
||||
export type TaxJarOrderConfirmedPayload = {
|
||||
order: OrderConfirmedSubscriptionFragment;
|
||||
};
|
||||
export type TaxJarOrderCreatedTarget = CreateOrderArgs;
|
||||
export type TaxJarOrderCreatedResponse = CreateOrderResponse;
|
||||
export type TaxJarOrderConfirmedTarget = CreateOrderArgs;
|
||||
export type TaxJarOrderConfirmedResponse = CreateOrderResponse;
|
||||
|
||||
export class TaxJarOrderCreatedAdapter
|
||||
implements WebhookAdapter<TaxJarOrderCreatedPayload, TaxJarOrderCreatedResponse>
|
||||
export class TaxJarOrderConfirmedAdapter
|
||||
implements WebhookAdapter<TaxJarOrderConfirmedPayload, TaxJarOrderConfirmedResponse>
|
||||
{
|
||||
private logger: Logger;
|
||||
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...");
|
||||
const payloadService = new TaxJarOrderCreatedPayloadService(this.authData);
|
||||
const payloadService = new TaxJarOrderConfirmedPayloadService(this.authData);
|
||||
const target = await payloadService.getPayload(payload.order, this.config);
|
||||
|
||||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
||||
|
@ -33,7 +33,7 @@ export class TaxJarOrderCreatedAdapter
|
|||
const response = await client.createOrder(target);
|
||||
|
||||
this.logger.debug("TaxJar createOrder successfully responded");
|
||||
const responseTransformer = new TaxJarOrderCreatedResponseTransformer();
|
||||
const responseTransformer = new TaxJarOrderConfirmedResponseTransformer();
|
||||
const transformedResponse = responseTransformer.transform(response);
|
||||
|
||||
this.logger.debug("Transformed TaxJar createOrder response");
|
|
@ -1,11 +1,11 @@
|
|||
import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
|
||||
import { OrderCreatedSubscriptionFragment, OrderStatus } from "../../../../generated/graphql";
|
||||
import { ChannelConfig } from "../../channel-configuration/channel-config";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
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 { TaxJarConfig } from "../taxjar-connection-schema";
|
||||
|
||||
type Order = OrderCreatedSubscriptionFragment;
|
||||
type Order = OrderConfirmedSubscriptionFragment;
|
||||
|
||||
// providerConfigMockGenerator class that other classes extend?
|
||||
const defaultProviderConfig: TaxJarConfig = {
|
||||
|
@ -23,7 +23,7 @@ const defaultProviderConfig: TaxJarConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const defaultOrderCreatedResponse: CreateOrderRes = {
|
||||
const defaultOrderConfirmedResponse: CreateOrderRes = {
|
||||
order: {
|
||||
user_id: 314973,
|
||||
transaction_reference_id: null,
|
||||
|
@ -82,14 +82,14 @@ const defaultOrderCreatedResponse: CreateOrderRes = {
|
|||
const testingScenariosMap = {
|
||||
default: {
|
||||
order: defaultOrder,
|
||||
response: defaultOrderCreatedResponse,
|
||||
response: defaultOrderConfirmedResponse,
|
||||
providerConfig: defaultProviderConfig,
|
||||
},
|
||||
};
|
||||
|
||||
type TestingScenario = keyof typeof testingScenariosMap;
|
||||
|
||||
export class TaxJarOrderCreatedMockGenerator {
|
||||
export class TaxJarOrderConfirmedMockGenerator {
|
||||
constructor(private scenario: TestingScenario = "default") {}
|
||||
generateOrder = (overrides: Partial<Order> = {}): Order =>
|
||||
structuredClone({
|
|
@ -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 { 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",
|
||||
productName: "Test product",
|
||||
|
@ -67,7 +67,7 @@ const matches: TaxJarTaxCodeMatches = [
|
|||
},
|
||||
];
|
||||
|
||||
describe("TaxJarOrderCreatedPayloadLinesTransformer", () => {
|
||||
describe("TaxJarOrderConfirmedPayloadLinesTransformer", () => {
|
||||
it("should map payload lines correctly", () => {
|
||||
expect(transformer.transform(mockedLines, matches)).toEqual([
|
||||
{
|
|
@ -1,15 +1,15 @@
|
|||
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 { TaxJarOrderCreatedTaxCodeMatcher } from "./taxjar-order-created-tax-code-matcher";
|
||||
import { TaxJarOrderConfirmedTaxCodeMatcher } from "./taxjar-order-confirmed-tax-code-matcher";
|
||||
|
||||
export class TaxJarOrderCreatedPayloadLinesTransformer {
|
||||
export class TaxJarOrderConfirmedPayloadLinesTransformer {
|
||||
transform(
|
||||
lines: OrderCreatedSubscriptionFragment["lines"],
|
||||
lines: OrderConfirmedSubscriptionFragment["lines"],
|
||||
matches: TaxJarTaxCodeMatches
|
||||
): LineItem[] {
|
||||
return lines.map((line) => {
|
||||
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
||||
const matcher = new TaxJarOrderConfirmedTaxCodeMatcher();
|
||||
const taxCode = matcher.match(line, matches);
|
||||
|
||||
return {
|
|
@ -1,17 +1,17 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { TaxJarOrderCreatedMockGenerator } from "./taxjar-order-created-mock-generator";
|
||||
import { TaxJarOrderConfirmedMockGenerator } from "./taxjar-order-confirmed-mock-generator";
|
||||
import {
|
||||
TaxJarOrderCreatedPayloadTransformer,
|
||||
TaxJarOrderConfirmedPayloadTransformer,
|
||||
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", () => {
|
||||
const orderMock = mockGenerator.generateOrder();
|
||||
const providerConfig = mockGenerator.generateProviderConfig();
|
||||
const transformer = new TaxJarOrderCreatedPayloadTransformer();
|
||||
const transformer = new TaxJarOrderConfirmedPayloadTransformer();
|
||||
const transformedPayload = transformer.transform(orderMock, providerConfig, []);
|
||||
|
||||
expect(transformedPayload.params.amount).toBe(239.17);
|
|
@ -1,11 +1,11 @@
|
|||
import { LineItem } from "taxjar/dist/util/types";
|
||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { numbers } from "../../taxes/numbers";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||
import { TaxJarOrderCreatedTarget } from "./taxjar-order-created-adapter";
|
||||
import { TaxJarOrderCreatedPayloadLinesTransformer } from "./taxjar-order-created-payload-lines-transformer";
|
||||
import { TaxJarOrderConfirmedTarget } from "./taxjar-order-confirmed-adapter";
|
||||
import { TaxJarOrderConfirmedPayloadLinesTransformer } from "./taxjar-order-confirmed-payload-lines-transformer";
|
||||
|
||||
export function sumPayloadLines(lines: LineItem[]): number {
|
||||
return numbers.roundFloatToTwoDecimals(
|
||||
|
@ -25,13 +25,13 @@ export function sumPayloadLines(lines: LineItem[]): number {
|
|||
);
|
||||
}
|
||||
|
||||
export class TaxJarOrderCreatedPayloadTransformer {
|
||||
export class TaxJarOrderConfirmedPayloadTransformer {
|
||||
transform(
|
||||
order: OrderCreatedSubscriptionFragment,
|
||||
order: OrderConfirmedSubscriptionFragment,
|
||||
taxJarConfig: TaxJarConfig,
|
||||
matches: TaxJarTaxCodeMatches
|
||||
): TaxJarOrderCreatedTarget {
|
||||
const linesTransformer = new TaxJarOrderCreatedPayloadLinesTransformer();
|
||||
): TaxJarOrderConfirmedTarget {
|
||||
const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
|
||||
const lineItems = linesTransformer.transform(order.lines, matches);
|
||||
const lineSum = sumPayloadLines(lineItems);
|
||||
const shippingAmount = order.shippingPrice.gross.amount;
|
|
@ -1,11 +1,11 @@
|
|||
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 { 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";
|
||||
|
||||
export class TaxJarOrderCreatedPayloadService {
|
||||
export class TaxJarOrderConfirmedPayloadService {
|
||||
constructor(private authData: AuthData) {}
|
||||
|
||||
private getMatches() {
|
||||
|
@ -15,11 +15,11 @@ export class TaxJarOrderCreatedPayloadService {
|
|||
}
|
||||
|
||||
async getPayload(
|
||||
order: OrderCreatedSubscriptionFragment,
|
||||
order: OrderConfirmedSubscriptionFragment,
|
||||
taxJarConfig: TaxJarConfig
|
||||
): Promise<CreateOrderArgs> {
|
||||
const matches = await this.getMatches();
|
||||
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer();
|
||||
const payloadTransformer = new TaxJarOrderConfirmedPayloadTransformer();
|
||||
|
||||
return payloadTransformer.transform(order, taxJarConfig, matches);
|
||||
}
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
|
||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||
|
||||
export class TaxJarOrderCreatedResponseTransformer {
|
||||
export class TaxJarOrderConfirmedResponseTransformer {
|
||||
transform(response: CreateOrderRes): CreateOrderResponse {
|
||||
return {
|
||||
id: response.order.transaction_id,
|
|
@ -1,6 +1,6 @@
|
|||
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||
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";
|
||||
|
||||
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", () => {
|
||||
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
||||
const matcher = new TaxJarOrderConfirmedTaxCodeMatcher();
|
||||
|
||||
expect(matcher.match(mockedLine, matches)).toEqual("");
|
||||
});
|
||||
it("should return tax code if tax class is found", () => {
|
||||
const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } });
|
||||
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
||||
const matcher = new TaxJarOrderConfirmedTaxCodeMatcher();
|
||||
|
||||
expect(matcher.match(line, matches)).toEqual("P0000000");
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||
|
||||
export class TaxJarOrderCreatedTaxCodeMatcher {
|
||||
export class TaxJarOrderConfirmedTaxCodeMatcher {
|
||||
private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) {
|
||||
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import { describe } from "vitest";
|
||||
|
||||
describe.todo("TaxJarOrderCreatedAdapter", () => {});
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,10 +1,14 @@
|
|||
import { OrderCreatedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
||||
import {
|
||||
OrderCancelledEventSubscriptionFragment,
|
||||
OrderConfirmedSubscriptionFragment,
|
||||
TaxBaseFragment,
|
||||
} from "../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../lib/logger";
|
||||
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
||||
import { TaxJarClient } from "./taxjar-client";
|
||||
import { TaxJarConfig } from "./taxjar-connection-schema";
|
||||
import { TaxJarOrderCreatedAdapter } from "./order-created/taxjar-order-created-adapter";
|
||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
||||
import { CreateOrderResponse, ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
|
||||
export class TaxJarWebhookService implements ProviderWebhookService {
|
||||
|
@ -30,8 +34,8 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
|||
return response;
|
||||
}
|
||||
|
||||
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||
const adapter = new TaxJarOrderCreatedAdapter(this.config, this.authData);
|
||||
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
|
||||
const adapter = new TaxJarOrderConfirmedAdapter(this.config, this.authData);
|
||||
|
||||
const response = await adapter.send({ order });
|
||||
|
||||
|
@ -42,4 +46,8 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
|||
async fulfillOrder() {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
|
||||
// TaxJar isn't implemented yet
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import { AppManifest } from "@saleor/app-sdk/types";
|
|||
import packageJson from "../../../package.json";
|
||||
import { checkoutCalculateTaxesSyncWebhook } from "./webhooks/checkout-calculate-taxes";
|
||||
import { orderCalculateTaxesSyncWebhook } from "./webhooks/order-calculate-taxes";
|
||||
import { orderCreatedAsyncWebhook } from "./webhooks/order-created";
|
||||
import { orderFulfilledAsyncWebhook } from "./webhooks/order-fulfilled";
|
||||
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
|
||||
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
|
||||
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
|
||||
|
||||
export default createManifestHandler({
|
||||
async manifestFactory({ appBaseUrl }) {
|
||||
|
@ -35,8 +35,8 @@ export default createManifestHandler({
|
|||
webhooks: [
|
||||
orderCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderCreatedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderFulfilledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -46,12 +46,7 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res,
|
|||
try {
|
||||
verifyCalculateTaxesPayload(payload);
|
||||
logger.debug("Payload validated succesfully");
|
||||
} catch (error) {
|
||||
logger.debug("Payload validation failed");
|
||||
return webhookResponse.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
||||
const channelSlug = payload.taxBase.channel.slug;
|
||||
const activeConnectionService = getActiveConnectionService(
|
||||
|
|
|
@ -46,12 +46,7 @@ export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx
|
|||
try {
|
||||
verifyCalculateTaxesPayload(payload);
|
||||
logger.debug("Payload validated succesfully");
|
||||
} catch (error) {
|
||||
logger.debug("Payload validation failed");
|
||||
return webhookResponse.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
||||
const channelSlug = payload.taxBase.channel.slug;
|
||||
const activeConnectionService = getActiveConnectionService(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
OrderFulfilledEventSubscriptionFragment,
|
||||
UntypedOrderFulfilledSubscriptionDocument,
|
||||
OrderCancelledEventSubscriptionFragment,
|
||||
UntypedOrderCancelledSubscriptionDocument,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../saleor-app";
|
||||
import { createLogger } from "../../../lib/logger";
|
||||
|
@ -13,43 +13,43 @@ export const config = {
|
|||
},
|
||||
};
|
||||
|
||||
type OrderFulfilledPayload = Extract<
|
||||
OrderFulfilledEventSubscriptionFragment,
|
||||
{ __typename: "OrderFulfilled" }
|
||||
export type OrderCancelledPayload = Extract<
|
||||
OrderCancelledEventSubscriptionFragment,
|
||||
{ __typename: "OrderCancelled" }
|
||||
>;
|
||||
|
||||
export const orderFulfilledAsyncWebhook = new SaleorAsyncWebhook<OrderFulfilledPayload>({
|
||||
name: "OrderFulfilled",
|
||||
export const orderCancelledAsyncWebhook = new SaleorAsyncWebhook<OrderCancelledPayload>({
|
||||
name: "OrderCancelled",
|
||||
apl: saleorApp.apl,
|
||||
event: "ORDER_FULFILLED",
|
||||
query: UntypedOrderFulfilledSubscriptionDocument,
|
||||
webhookPath: "/api/webhooks/order-fulfilled",
|
||||
event: "ORDER_CANCELLED",
|
||||
query: UntypedOrderCancelledSubscriptionDocument,
|
||||
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 { payload } = ctx;
|
||||
const webhookResponse = new WebhookResponse(res);
|
||||
|
||||
logger.info("Handler called with payload");
|
||||
|
||||
if (!payload.order) {
|
||||
return webhookResponse.error(new Error("Insufficient order data"));
|
||||
}
|
||||
|
||||
try {
|
||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
||||
const channelSlug = payload.order?.channel.slug;
|
||||
const channelSlug = payload.order.channel.slug;
|
||||
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
|
||||
if (!payload.order) {
|
||||
return webhookResponse.error(new Error("Insufficient order data"));
|
||||
}
|
||||
await taxProvider.fulfillOrder(payload.order);
|
||||
await taxProvider.cancelOrder(payload);
|
||||
|
||||
logger.info("Order fulfilled");
|
||||
logger.info("Order cancelled");
|
||||
|
||||
return webhookResponse.success();
|
||||
} catch (error) {
|
||||
return webhookResponse.error(new Error("Error while fulfilling tax provider order"));
|
||||
return webhookResponse.error(new Error("Error while cancelling tax provider order"));
|
||||
}
|
||||
});
|
|
@ -1,19 +1,15 @@
|
|||
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
OrderCreatedEventSubscriptionFragment,
|
||||
OrderConfirmedEventSubscriptionFragment,
|
||||
OrderStatus,
|
||||
UntypedOrderCreatedSubscriptionDocument,
|
||||
UpdateMetadataDocument,
|
||||
UpdateMetadataMutation,
|
||||
UpdateMetadataMutationVariables,
|
||||
UntypedOrderConfirmedSubscriptionDocument,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../saleor-app";
|
||||
import { createLogger } from "../../../lib/logger";
|
||||
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
|
||||
import { Client } from "urql";
|
||||
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 { OrderMetadataManager } from "../../../modules/app/order-metadata-manager";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
|
@ -21,49 +17,20 @@ export const config = {
|
|||
},
|
||||
};
|
||||
|
||||
type OrderCreatedPayload = Extract<
|
||||
OrderCreatedEventSubscriptionFragment,
|
||||
{ __typename: "OrderCreated" }
|
||||
type OrderConfirmedPayload = Extract<
|
||||
OrderConfirmedEventSubscriptionFragment,
|
||||
{ __typename: "OrderConfirmed" }
|
||||
>;
|
||||
|
||||
export const orderCreatedAsyncWebhook = new SaleorAsyncWebhook<OrderCreatedPayload>({
|
||||
name: "OrderCreated",
|
||||
export const orderConfirmedAsyncWebhook = new SaleorAsyncWebhook<OrderConfirmedPayload>({
|
||||
name: "OrderConfirmed",
|
||||
apl: saleorApp.apl,
|
||||
event: "ORDER_CREATED",
|
||||
query: UntypedOrderCreatedSubscriptionDocument,
|
||||
webhookPath: "/api/webhooks/order-created",
|
||||
event: "ORDER_CONFIRMED",
|
||||
query: UntypedOrderConfirmedSubscriptionDocument,
|
||||
webhookPath: "/api/webhooks/order-confirmed",
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
export default orderConfirmedAsyncWebhook.createHandler(async (req, res, ctx) => {
|
||||
const logger = createLogger({ event: ctx.event });
|
||||
const { payload, authData } = ctx;
|
||||
const { saleorApiUrl, token } = authData;
|
||||
|
@ -76,8 +43,6 @@ export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => {
|
|||
const channelSlug = payload.order?.channel.slug;
|
||||
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
|
||||
|
||||
logger.info("Fetched taxProvider");
|
||||
|
||||
// todo: figure out what fields are needed and add validation
|
||||
if (!payload.order) {
|
||||
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"));
|
||||
}
|
||||
|
||||
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({
|
||||
saleorApiUrl,
|
||||
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");
|
||||
|
||||
return webhookResponse.success();
|
|
@ -1337,8 +1337,8 @@ importers:
|
|||
specifier: ^2.1.4
|
||||
version: 2.1.4(graphql@16.7.1)
|
||||
avatax:
|
||||
specifier: ^23.3.2
|
||||
version: 23.3.2
|
||||
specifier: ^23.7.0
|
||||
version: 23.7.0
|
||||
clsx:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
|
@ -11256,8 +11256,8 @@ packages:
|
|||
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
/avatax@23.3.2:
|
||||
resolution: {integrity: sha512-WGjSIQNvdM0XSyukjb+QIxB1z4xaEOdaGmPhXwLJYgxIuJlDw7Uq1nHUHo+yeHKZk3dKGzCyrFaa8N1Wx9nSbQ==}
|
||||
/avatax@23.7.0:
|
||||
resolution: {integrity: sha512-34CuafkIgzfj6B8gTMmeUN5iYWdgHtSKRwIRWIYFcP/OpUMDOGB0WMMdGrZUgthLjYCWOkQdi2QReWuM/TSROg==}
|
||||
dependencies:
|
||||
'@types/node-fetch': 2.6.4
|
||||
form-data: 4.0.0
|
||||
|
|
Loading…
Reference in a new issue