fix checkout calculate taxes (#919)
* fix: 🐛 value of customerCode in calculateTaxes * build: 👷 add changeset * fix: 🐛 tests
This commit is contained in:
parent
4a635620c4
commit
45ed9fb444
17 changed files with 114 additions and 63 deletions
5
.changeset/unlucky-tips-smash.md
Normal file
5
.changeset/unlucky-tips-smash.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-taxes": patch
|
||||
---
|
||||
|
||||
Fixed the error when checkout couldn't calculate taxes when no customerCode was provided. In calculate taxes, the customerCode is now derived from issuingPrincipal's id.
|
|
@ -10,5 +10,11 @@ fragment CalculateTaxesEvent on Event {
|
|||
value
|
||||
}
|
||||
}
|
||||
issuingPrincipal {
|
||||
__typename
|
||||
... on User {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,15 +62,9 @@ fragment TaxBase on TaxableObject {
|
|||
sourceObject {
|
||||
... on Checkout {
|
||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on Order {
|
||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
|
|||
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
||||
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
||||
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
export class AvataxWebhookService implements ProviderWebhookService {
|
||||
config = defaultAvataxConfig;
|
||||
|
@ -27,10 +28,10 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
|||
this.client = avataxClient;
|
||||
}
|
||||
|
||||
async calculateTaxes(taxBase: TaxBaseFragment) {
|
||||
async calculateTaxes(payload: CalculateTaxesPayload) {
|
||||
const adapter = new AvataxCalculateTaxesAdapter(this.config, this.authData);
|
||||
|
||||
const response = await adapter.send({ taxBase });
|
||||
const response = await adapter.send(payload);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../../lib/logger";
|
||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||
|
@ -10,26 +10,27 @@ import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxe
|
|||
|
||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||
|
||||
export type AvataxCalculateTaxesPayload = {
|
||||
taxBase: TaxBaseFragment;
|
||||
};
|
||||
|
||||
export type AvataxCalculateTaxesTarget = CreateTransactionArgs;
|
||||
export type AvataxCalculateTaxesResponse = CalculateTaxesResponse;
|
||||
|
||||
export class AvataxCalculateTaxesAdapter
|
||||
implements WebhookAdapter<AvataxCalculateTaxesPayload, AvataxCalculateTaxesResponse>
|
||||
implements WebhookAdapter<CalculateTaxesPayload, AvataxCalculateTaxesResponse>
|
||||
{
|
||||
private logger: Logger;
|
||||
constructor(private readonly config: AvataxConfig, private authData: AuthData) {
|
||||
constructor(
|
||||
private readonly config: AvataxConfig,
|
||||
private authData: AuthData,
|
||||
) {
|
||||
this.logger = createLogger({ name: "AvataxCalculateTaxesAdapter" });
|
||||
}
|
||||
|
||||
// todo: refactor because its getting too big
|
||||
async send(payload: AvataxCalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
|
||||
this.logger.debug("Transforming the Saleor payload for calculating taxes with AvaTax...");
|
||||
async send(payload: CalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
|
||||
this.logger.debug(
|
||||
{ payload },
|
||||
"Transforming the Saleor payload for calculating taxes with AvaTax...",
|
||||
);
|
||||
const payloadService = new AvataxCalculateTaxesPayloadService(this.authData);
|
||||
const target = await payloadService.getPayload(payload.taxBase, this.config);
|
||||
const target = await payloadService.getPayload(payload, this.config);
|
||||
|
||||
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
||||
|
||||
|
|
|
@ -107,9 +107,6 @@ const defaultTaxBase: TaxBase = {
|
|||
],
|
||||
sourceObject: {
|
||||
avataxEntityCode: null,
|
||||
user: {
|
||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import { AvataxCalculateTaxesMockGenerator } from "./avatax-calculate-taxes-mock-generator";
|
||||
import { AvataxCalculateTaxesPayloadTransformer } from "./avatax-calculate-taxes-payload-transformer";
|
||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
const mockGenerator = new AvataxCalculateTaxesMockGenerator();
|
||||
const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
||||
|
@ -10,11 +11,18 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
|||
it("returns document type of SalesInvoice", async () => {
|
||||
const taxBaseMock = mockGenerator.generateTaxBase();
|
||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||
const payloadMock = {
|
||||
taxBase: taxBaseMock,
|
||||
issuingPrincipal: {
|
||||
__typename: "User",
|
||||
id: "1",
|
||||
},
|
||||
} as unknown as CalculateTaxesPayload;
|
||||
|
||||
const payload = await new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||
taxBaseMock,
|
||||
payloadMock,
|
||||
avataxConfigMock,
|
||||
matchesMock
|
||||
matchesMock,
|
||||
);
|
||||
|
||||
expect(payload.model.type).toBe(DocumentType.SalesOrder);
|
||||
|
@ -22,27 +30,56 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
|||
it("when discounts, calculates the sum of discounts", async () => {
|
||||
const taxBaseMock = mockGenerator.generateTaxBase({ discounts: [{ amount: { amount: 10 } }] });
|
||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||
const payloadMock = {
|
||||
taxBase: taxBaseMock,
|
||||
issuingPrincipal: {
|
||||
__typename: "User",
|
||||
id: "1",
|
||||
},
|
||||
} as unknown as CalculateTaxesPayload;
|
||||
|
||||
const payload = await new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||
taxBaseMock,
|
||||
payloadMock,
|
||||
avataxConfigMock,
|
||||
matchesMock
|
||||
matchesMock,
|
||||
);
|
||||
|
||||
expect(payload.model.discount).toEqual(10);
|
||||
});
|
||||
it("when no discounts, the sum of discount is 0", async () => {
|
||||
const mockGenerator = new AvataxCalculateTaxesMockGenerator();
|
||||
const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
||||
const taxBaseMock = mockGenerator.generateTaxBase();
|
||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||
|
||||
const payloadMock = {
|
||||
taxBase: taxBaseMock,
|
||||
issuingPrincipal: {
|
||||
__typename: "User",
|
||||
id: "1",
|
||||
},
|
||||
} as unknown as CalculateTaxesPayload;
|
||||
|
||||
const payload = await new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||
taxBaseMock,
|
||||
payloadMock,
|
||||
avataxConfigMock,
|
||||
matchesMock
|
||||
matchesMock,
|
||||
);
|
||||
|
||||
expect(payload.model.discount).toEqual(0);
|
||||
});
|
||||
it("when no issuingPrincipal.id, throws an error", async () => {
|
||||
const taxBaseMock = mockGenerator.generateTaxBase();
|
||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||
|
||||
const payloadMock = {
|
||||
taxBase: taxBaseMock,
|
||||
} as unknown as CalculateTaxesPayload;
|
||||
|
||||
await expect(
|
||||
new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||
payloadMock,
|
||||
avataxConfigMock,
|
||||
matchesMock,
|
||||
),
|
||||
).rejects.toThrow("This field must be defined.");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@ import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
|||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||
import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer";
|
||||
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
export class AvataxCalculateTaxesPayloadTransformer {
|
||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
||||
|
@ -20,32 +22,38 @@ export class AvataxCalculateTaxesPayloadTransformer {
|
|||
}
|
||||
|
||||
async transform(
|
||||
taxBase: TaxBaseFragment,
|
||||
payload: CalculateTaxesPayload,
|
||||
avataxConfig: AvataxConfig,
|
||||
matches: AvataxTaxCodeMatches
|
||||
matches: AvataxTaxCodeMatches,
|
||||
): Promise<CreateTransactionArgs> {
|
||||
const payloadLinesTransformer = new AvataxCalculateTaxesPayloadLinesTransformer();
|
||||
const avataxClient = new AvataxClient(avataxConfig);
|
||||
const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient });
|
||||
const entityUseCode = await entityTypeMatcher.match(taxBase.sourceObject.avataxEntityCode);
|
||||
const entityUseCode = await entityTypeMatcher.match(
|
||||
payload.taxBase.sourceObject.avataxEntityCode,
|
||||
);
|
||||
|
||||
const customerCode = taxProviderUtils.resolveStringOrThrow(
|
||||
payload.issuingPrincipal?.__typename === "User" ? payload.issuingPrincipal.id : undefined,
|
||||
);
|
||||
|
||||
return {
|
||||
model: {
|
||||
type: this.matchDocumentType(avataxConfig),
|
||||
entityUseCode,
|
||||
customerCode: taxBase.sourceObject.user?.id ?? "",
|
||||
customerCode,
|
||||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||
commit: avataxConfig.isAutocommit,
|
||||
addresses: {
|
||||
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
|
||||
shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!),
|
||||
shipTo: avataxAddressFactory.fromSaleorAddress(payload.taxBase.address!),
|
||||
},
|
||||
currencyCode: taxBase.currency,
|
||||
lines: payloadLinesTransformer.transform(taxBase, avataxConfig, matches),
|
||||
currencyCode: payload.taxBase.currency,
|
||||
lines: payloadLinesTransformer.transform(payload.taxBase, avataxConfig, matches),
|
||||
date: new Date(),
|
||||
discount: discountUtils.sumDiscounts(
|
||||
taxBase.discounts.map((discount) => discount.amount.amount)
|
||||
payload.taxBase.discounts.map((discount) => discount.amount.amount),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import { CreateTransactionArgs } from "../avatax-client";
|
|||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxTaxCodeMatchesService } from "../tax-code/avatax-tax-code-matches.service";
|
||||
import { AvataxCalculateTaxesPayloadTransformer } from "./avatax-calculate-taxes-payload-transformer";
|
||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
export class AvataxCalculateTaxesPayloadService {
|
||||
constructor(private authData: AuthData) {}
|
||||
|
@ -15,12 +16,12 @@ export class AvataxCalculateTaxesPayloadService {
|
|||
}
|
||||
|
||||
async getPayload(
|
||||
taxBase: TaxBaseFragment,
|
||||
avataxConfig: AvataxConfig
|
||||
payload: CalculateTaxesPayload,
|
||||
avataxConfig: AvataxConfig,
|
||||
): Promise<CreateTransactionArgs> {
|
||||
const matches = await this.getMatches();
|
||||
const payloadTransformer = new AvataxCalculateTaxesPayloadTransformer();
|
||||
|
||||
return payloadTransformer.transform(taxBase, avataxConfig, matches);
|
||||
return payloadTransformer.transform(payload, avataxConfig, matches);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
|||
async transform(
|
||||
order: OrderConfirmedSubscriptionFragment,
|
||||
avataxConfig: AvataxConfig,
|
||||
matches: AvataxTaxCodeMatches
|
||||
matches: AvataxTaxCodeMatches,
|
||||
): Promise<CreateTransactionArgs> {
|
||||
const avataxClient = new AvataxClient(avataxConfig);
|
||||
|
||||
|
@ -46,9 +46,7 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
|||
code,
|
||||
type: this.matchDocumentType(avataxConfig),
|
||||
entityUseCode,
|
||||
customerCode:
|
||||
order.user?.id ??
|
||||
"" /* In Saleor AvaTax plugin, the customer code is 0. In Taxes App, we set it to the user id. */,
|
||||
customerCode: taxProviderUtils.resolveStringOrThrow(order.user?.id),
|
||||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||
commit: avataxConfig.isAutocommit,
|
||||
|
@ -62,7 +60,7 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
|||
lines: linesTransformer.transform(order, avataxConfig, matches),
|
||||
date,
|
||||
discount: discountUtils.sumDiscounts(
|
||||
order.discounts.map((discount) => discount.amount.amount)
|
||||
order.discounts.map((discount) => discount.amount.amount),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ 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 { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
// todo: refactor to a factory
|
||||
class ActiveTaxProviderService implements ProviderWebhookService {
|
||||
|
@ -47,7 +48,7 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
|||
}
|
||||
}
|
||||
|
||||
async calculateTaxes(payload: TaxBaseFragment) {
|
||||
async calculateTaxes(payload: CalculateTaxesPayload) {
|
||||
return this.client.calculateTaxes(payload);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,10 @@ function resolveOptionalOrThrow<T>(value: T | undefined | null, error?: Error):
|
|||
}
|
||||
|
||||
function resolveStringOrThrow(value: string | undefined | null): string {
|
||||
return z.string().min(1, { message: "This field can not be empty." }).parse(value);
|
||||
return z
|
||||
.string({ required_error: "This field must be defined." })
|
||||
.min(1, { message: "This field can not be empty." })
|
||||
.parse(value);
|
||||
}
|
||||
|
||||
export const taxProviderUtils = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
||||
import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||
|
||||
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
||||
|
@ -7,7 +8,7 @@ export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TA
|
|||
export type CreateOrderResponse = { id: string };
|
||||
|
||||
export interface ProviderWebhookService {
|
||||
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
||||
calculateTaxes: (payload: CalculateTaxesPayload) => Promise<CalculateTaxesResponse>;
|
||||
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -96,9 +96,6 @@ const taxIncludedTaxBase: TaxBase = {
|
|||
],
|
||||
sourceObject: {
|
||||
avataxEntityCode: null,
|
||||
user: {
|
||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -194,9 +191,6 @@ const taxExcludedTaxBase: TaxBase = {
|
|||
],
|
||||
sourceObject: {
|
||||
avataxEntityCode: null,
|
||||
user: {
|
||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-
|
|||
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
||||
import { TaxJarClient } from "./taxjar-client";
|
||||
import { TaxJarConfig } from "./taxjar-connection-schema";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
export class TaxJarWebhookService implements ProviderWebhookService {
|
||||
client: TaxJarClient;
|
||||
|
@ -29,10 +30,10 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
|||
});
|
||||
}
|
||||
|
||||
async calculateTaxes(taxBase: TaxBaseFragment) {
|
||||
async calculateTaxes(payload: CalculateTaxesPayload) {
|
||||
const adapter = new TaxJarCalculateTaxesAdapter(this.config, this.authData);
|
||||
|
||||
const response = await adapter.send({ taxBase });
|
||||
const response = await adapter.send(payload);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,10 @@ export const config = {
|
|||
},
|
||||
};
|
||||
|
||||
type CalculateTaxesPayload = Extract<CalculateTaxesEventFragment, { __typename: "CalculateTaxes" }>;
|
||||
export type CalculateTaxesPayload = Extract<
|
||||
CalculateTaxesEventFragment,
|
||||
{ __typename: "CalculateTaxes" }
|
||||
>;
|
||||
|
||||
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
|
||||
if (!payload.taxBase.lines) {
|
||||
|
@ -52,11 +55,11 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res,
|
|||
const activeConnectionService = getActiveConnectionService(
|
||||
channelSlug,
|
||||
appMetadata,
|
||||
ctx.authData
|
||||
ctx.authData,
|
||||
);
|
||||
|
||||
logger.info("Found active connection service. Calculating taxes...");
|
||||
const calculatedTaxes = await activeConnectionService.calculateTaxes(payload.taxBase);
|
||||
const calculatedTaxes = await activeConnectionService.calculateTaxes(payload);
|
||||
|
||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
||||
return webhookResponse.success(ctx.buildResponse(calculatedTaxes));
|
||||
|
|
|
@ -52,11 +52,11 @@ export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx
|
|||
const activeConnectionService = getActiveConnectionService(
|
||||
channelSlug,
|
||||
appMetadata,
|
||||
ctx.authData
|
||||
ctx.authData,
|
||||
);
|
||||
|
||||
logger.info("Found active connection service. Calculating taxes...");
|
||||
const calculatedTaxes = await activeConnectionService.calculateTaxes(payload.taxBase);
|
||||
const calculatedTaxes = await activeConnectionService.calculateTaxes(payload);
|
||||
|
||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
||||
return webhookResponse.success(ctx.buildResponse(calculatedTaxes));
|
||||
|
|
Loading…
Reference in a new issue