Complete migration to OrderConfirmed flow (#916)
* refactor: 🔥 order-created & order-fulfilled code * build: 👷 add changeset * fix: 🐛 fix build
This commit is contained in:
parent
3bd2d33d97
commit
783bd5ec55
39 changed files with 22 additions and 3406 deletions
5
.changeset/nice-jobs-ring.md
Normal file
5
.changeset/nice-jobs-ring.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-taxes": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Removed all the code related to the deprecated OrderCreated & OrderFulfilled flow. The migration process began in version 1.13.0. All the cloud environments had been migrated automatically.
|
|
@ -1,89 +0,0 @@
|
||||||
fragment OrderLine on OrderLine {
|
|
||||||
productSku
|
|
||||||
productName
|
|
||||||
quantity
|
|
||||||
taxClass {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
unitPrice {
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalPrice {
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
tax {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment OrderCreatedSubscription on Order {
|
|
||||||
id
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
email
|
|
||||||
}
|
|
||||||
created
|
|
||||||
status
|
|
||||||
channel {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
shippingAddress {
|
|
||||||
...Address
|
|
||||||
}
|
|
||||||
billingAddress {
|
|
||||||
...Address
|
|
||||||
}
|
|
||||||
total {
|
|
||||||
currency
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
tax {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shippingPrice {
|
|
||||||
gross {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines {
|
|
||||||
...OrderLine
|
|
||||||
}
|
|
||||||
discounts {
|
|
||||||
id
|
|
||||||
amount {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
|
||||||
avataxTaxCalculationDate: metafield(key: "avataxTaxCalculationDate")
|
|
||||||
avataxDocumentCode: metafield(key: "avataxDocumentCode")
|
|
||||||
}
|
|
||||||
fragment OrderCreatedEventSubscription on Event {
|
|
||||||
__typename
|
|
||||||
... on OrderCreated {
|
|
||||||
order {
|
|
||||||
...OrderCreatedSubscription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recipient {
|
|
||||||
privateMetadata {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subscription OrderCreatedSubscription {
|
|
||||||
event {
|
|
||||||
...OrderCreatedEventSubscription
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
fragment OrderLine on OrderLine {
|
|
||||||
productSku
|
|
||||||
productName
|
|
||||||
quantity
|
|
||||||
taxClass {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
unitPrice {
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalPrice {
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
tax {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment OrderFulfilledSubscription on Order {
|
|
||||||
id
|
|
||||||
userEmail
|
|
||||||
created
|
|
||||||
channel {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
shippingAddress {
|
|
||||||
...Address
|
|
||||||
}
|
|
||||||
billingAddress {
|
|
||||||
...Address
|
|
||||||
}
|
|
||||||
total {
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
tax {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shippingPrice {
|
|
||||||
net {
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
avataxId: metafield(key: "avataxId")
|
|
||||||
lines {
|
|
||||||
...OrderLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fragment OrderFulfilledEventSubscription on Event {
|
|
||||||
__typename
|
|
||||||
... on OrderFulfilled {
|
|
||||||
order {
|
|
||||||
...OrderFulfilledSubscription
|
|
||||||
}
|
|
||||||
recipient {
|
|
||||||
privateMetadata {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subscription OrderFulfilledSubscription {
|
|
||||||
event {
|
|
||||||
...OrderFulfilledEventSubscription
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
||||||
OrderConfirmedSubscriptionFragment,
|
|
||||||
OrderCreatedSubscriptionFragment,
|
|
||||||
OrderFulfilledSubscriptionFragment,
|
|
||||||
TaxBaseFragment,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
|
@ -13,15 +8,16 @@ import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
|
||||||
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
||||||
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
||||||
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
||||||
import { AvataxOrderCreatedAdapter } from "./order-created/avatax-order-created-adapter";
|
|
||||||
import { AvataxOrderFulfilledAdapter } from "./order-fulfilled/avatax-order-fulfilled-adapter";
|
|
||||||
|
|
||||||
export class AvataxWebhookService implements ProviderWebhookService {
|
export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
config = defaultAvataxConfig;
|
config = defaultAvataxConfig;
|
||||||
client: AvataxClient;
|
client: AvataxClient;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(config: AvataxConfig, private authData: AuthData) {
|
constructor(
|
||||||
|
config: AvataxConfig,
|
||||||
|
private authData: AuthData,
|
||||||
|
) {
|
||||||
this.logger = createLogger({
|
this.logger = createLogger({
|
||||||
name: "AvataxWebhookService",
|
name: "AvataxWebhookService",
|
||||||
});
|
});
|
||||||
|
@ -47,31 +43,9 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
async fulfillOrder(order: OrderFulfilledSubscriptionFragment) {
|
|
||||||
const adapter = new AvataxOrderFulfilledAdapter(this.config);
|
|
||||||
|
|
||||||
const response = await adapter.send({ order });
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledPayload) {
|
async cancelOrder(payload: OrderCancelledPayload) {
|
||||||
const adapter = new AvataxOrderCancelledAdapter(this.config);
|
const adapter = new AvataxOrderCancelledAdapter(this.config);
|
||||||
|
|
||||||
await adapter.send(payload);
|
await adapter.send(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
async createOrder(payload: OrderCreatedSubscriptionFragment) {
|
|
||||||
const adapter = new AvataxOrderCreatedAdapter(this.config, this.authData);
|
|
||||||
|
|
||||||
const response = await adapter.send({ order: payload });
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { describe, it } from "vitest";
|
|
||||||
|
|
||||||
describe("AvataxOrderCreatedAdapter", () => {
|
|
||||||
// ? how to mock internal call to avatax?
|
|
||||||
it.todo("calls avatax client", () => {});
|
|
||||||
});
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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";
|
|
||||||
|
|
||||||
type AvataxOrderCreatedPayload = {
|
|
||||||
order: OrderCreatedSubscriptionFragment;
|
|
||||||
};
|
|
||||||
type AvataxOrderCreatedResponse = CreateOrderResponse;
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedAdapter
|
|
||||||
implements WebhookAdapter<AvataxOrderCreatedPayload, AvataxOrderCreatedResponse>
|
|
||||||
{
|
|
||||||
private logger: Logger;
|
|
||||||
|
|
||||||
constructor(private readonly config: AvataxConfig, private authData: AuthData) {
|
|
||||||
this.logger = createLogger({ name: "AvataxOrderCreatedAdapter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(payload: AvataxOrderCreatedPayload): Promise<AvataxOrderCreatedResponse> {
|
|
||||||
this.logger.debug("Transforming the Saleor payload for creating order with AvaTax...");
|
|
||||||
|
|
||||||
const payloadService = new AvataxOrderCreatedPayloadService(this.authData);
|
|
||||||
const target = await payloadService.getPayload(payload.order, this.config);
|
|
||||||
|
|
||||||
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
|
||||||
const response = await client.createTransaction(target);
|
|
||||||
|
|
||||||
this.logger.debug("AvaTax createTransaction successfully responded");
|
|
||||||
|
|
||||||
const responseTransformer = new AvataxOrderCreatedResponseTransformer();
|
|
||||||
const transformedResponse = responseTransformer.transform(response);
|
|
||||||
|
|
||||||
this.logger.debug("Transformed AvaTax createTransaction response");
|
|
||||||
|
|
||||||
return transformedResponse;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
|
||||||
import { ChannelConfig } from "../../channel-configuration/channel-config";
|
|
||||||
import { orderCreatedTransactionMock } from "./avatax-order-created-response-transaction-mock";
|
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
|
||||||
import { defaultOrder } from "../../../mocks";
|
|
||||||
import { AvataxConfigMockGenerator } from "../avatax-config-mock-generator";
|
|
||||||
|
|
||||||
const defaultChannelConfig: ChannelConfig = {
|
|
||||||
id: "1",
|
|
||||||
config: {
|
|
||||||
providerConnectionId: "aa5293e5-7f5d-4782-a619-222ead918e50",
|
|
||||||
slug: "default-channel",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultOrderCreatedResponse: TransactionModel = orderCreatedTransactionMock;
|
|
||||||
|
|
||||||
const testingScenariosMap = {
|
|
||||||
default: {
|
|
||||||
order: defaultOrder,
|
|
||||||
channelConfig: defaultChannelConfig,
|
|
||||||
response: defaultOrderCreatedResponse,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type TestingScenario = keyof typeof testingScenariosMap;
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedMockGenerator {
|
|
||||||
constructor(private scenario: TestingScenario = "default") {}
|
|
||||||
generateOrder = (
|
|
||||||
overrides: Partial<OrderCreatedSubscriptionFragment> = {}
|
|
||||||
): OrderCreatedSubscriptionFragment =>
|
|
||||||
structuredClone({
|
|
||||||
...testingScenariosMap[this.scenario].order,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
generateChannelConfig = (overrides: Partial<ChannelConfig> = {}): ChannelConfig =>
|
|
||||||
structuredClone({
|
|
||||||
...testingScenariosMap[this.scenario].channelConfig,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
generateAvataxConfig = (overrides: Partial<AvataxConfig> = {}): AvataxConfig => {
|
|
||||||
const mockGenerator = new AvataxConfigMockGenerator();
|
|
||||||
|
|
||||||
return mockGenerator.generateAvataxConfig(overrides);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateResponse = (overrides: Partial<TransactionModel> = {}): TransactionModel =>
|
|
||||||
structuredClone({
|
|
||||||
...testingScenariosMap[this.scenario].response,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
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 { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
|
||||||
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator";
|
|
||||||
|
|
||||||
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
|
||||||
const mockGenerator = new AvataxOrderCreatedMockGenerator();
|
|
||||||
const orderMock = mockGenerator.generateOrder();
|
|
||||||
|
|
||||||
const matches: AvataxTaxCodeMatches = [];
|
|
||||||
|
|
||||||
describe("AvataxOrderCreatedPayloadLinesTransformer", () => {
|
|
||||||
const lines = linesTransformer.transform(orderMock, avataxConfigMock, matches);
|
|
||||||
|
|
||||||
it("returns the correct number of lines", () => {
|
|
||||||
expect(lines).toHaveLength(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes shipping as a line", () => {
|
|
||||||
expect(lines).toContainEqual({
|
|
||||||
itemCode: "Shipping",
|
|
||||||
taxCode: "FR000000",
|
|
||||||
quantity: 1,
|
|
||||||
amount: 59.17,
|
|
||||||
taxIncluded: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes products as lines", () => {
|
|
||||||
const [first, second, third] = lines;
|
|
||||||
|
|
||||||
expect(first).toContain({
|
|
||||||
itemCode: "328223580",
|
|
||||||
description: "Monospace Tee",
|
|
||||||
quantity: 3,
|
|
||||||
amount: 65.18,
|
|
||||||
});
|
|
||||||
expect(second).toContain({
|
|
||||||
itemCode: "328223581",
|
|
||||||
description: "Monospace Tee",
|
|
||||||
quantity: 1,
|
|
||||||
amount: 21.73,
|
|
||||||
});
|
|
||||||
expect(third).toContain({
|
|
||||||
itemCode: "118223581",
|
|
||||||
description: "Paul's Balance 420",
|
|
||||||
quantity: 2,
|
|
||||||
amount: 108.63,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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";
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedPayloadLinesTransformer {
|
|
||||||
transform(
|
|
||||||
order: OrderCreatedSubscriptionFragment,
|
|
||||||
config: AvataxConfig,
|
|
||||||
matches: AvataxTaxCodeMatches
|
|
||||||
): LineItemModel[] {
|
|
||||||
const productLines: LineItemModel[] = order.lines.map((line) => {
|
|
||||||
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
|
||||||
const taxCode = matcher.match(line, matches);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// taxes are included because we treat what is passed in payload as the source of truth
|
|
||||||
taxIncluded: true,
|
|
||||||
amount: numbers.roundFloatToTwoDecimals(
|
|
||||||
line.totalPrice.net.amount + line.totalPrice.tax.amount
|
|
||||||
),
|
|
||||||
taxCode,
|
|
||||||
quantity: line.quantity,
|
|
||||||
description: line.productName,
|
|
||||||
itemCode: line.productSku ?? "",
|
|
||||||
discounted: order.discounts.length > 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (order.shippingPrice.net.amount !== 0) {
|
|
||||||
// * In AvaTax, shipping is a regular line
|
|
||||||
const shippingLine: LineItemModel = {
|
|
||||||
amount: order.shippingPrice.gross.amount,
|
|
||||||
taxIncluded: true,
|
|
||||||
itemCode: SHIPPING_ITEM_CODE,
|
|
||||||
/**
|
|
||||||
* * Different shipping methods can have different tax codes.
|
|
||||||
* https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\
|
|
||||||
*/
|
|
||||||
taxCode: config.shippingTaxCode,
|
|
||||||
quantity: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return [...productLines, shippingLine];
|
|
||||||
}
|
|
||||||
|
|
||||||
return productLines;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator";
|
|
||||||
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|
||||||
|
|
||||||
const mockGenerator = new AvataxOrderCreatedMockGenerator();
|
|
||||||
|
|
||||||
const orderMock = mockGenerator.generateOrder();
|
|
||||||
const discountedOrderMock = mockGenerator.generateOrder({
|
|
||||||
discounts: [
|
|
||||||
{
|
|
||||||
amount: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
id: "RGlzY291bnREaXNjb3VudDox",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
|
||||||
|
|
||||||
export const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
|
||||||
|
|
||||||
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
|
||||||
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", async () => {
|
|
||||||
const payload = await transformer.transform(orderMock, avataxConfigMock, []);
|
|
||||||
|
|
||||||
expect(payload.model.type).toBe(DocumentType.SalesInvoice);
|
|
||||||
}),
|
|
||||||
it("returns document type of SalesOrder when isDocumentRecordingEnabled is false", async () => {
|
|
||||||
const payload = await transformer.transform(
|
|
||||||
orderMock,
|
|
||||||
{
|
|
||||||
...avataxConfigMock,
|
|
||||||
isDocumentRecordingEnabled: false,
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(payload.model.type).toBe(DocumentType.SalesOrder);
|
|
||||||
});
|
|
||||||
it("returns lines with discounted: true when there are discounts", async () => {
|
|
||||||
const payload = await transformer.transform(discountedOrderMock, avataxConfigMock, []);
|
|
||||||
|
|
||||||
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
|
||||||
const check = linesWithoutShipping.every((line) => line.discounted === true);
|
|
||||||
|
|
||||||
expect(check).toBe(true);
|
|
||||||
});
|
|
||||||
it("returns lines with discounted: false when there are no discounts", async () => {
|
|
||||||
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
|
||||||
const payload = await transformer.transform(orderMock, avataxConfigMock, []);
|
|
||||||
|
|
||||||
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
|
||||||
const check = linesWithoutShipping.every((line) => line.discounted === false);
|
|
||||||
|
|
||||||
expect(check).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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 { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
|
||||||
import { AvataxCalculationDateResolver } from "../avatax-calculation-date-resolver";
|
|
||||||
import { AvataxDocumentCodeResolver } from "../avatax-document-code-resolver";
|
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
|
||||||
|
|
||||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedPayloadTransformer {
|
|
||||||
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.
|
|
||||||
return DocumentType.SalesOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DocumentType.SalesInvoice;
|
|
||||||
}
|
|
||||||
async transform(
|
|
||||||
order: OrderCreatedSubscriptionFragment,
|
|
||||||
avataxConfig: AvataxConfig,
|
|
||||||
matches: AvataxTaxCodeMatches
|
|
||||||
): Promise<CreateTransactionArgs> {
|
|
||||||
const avataxClient = new AvataxClient(avataxConfig);
|
|
||||||
|
|
||||||
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
|
||||||
const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient });
|
|
||||||
const dateResolver = new AvataxCalculationDateResolver();
|
|
||||||
const documentCodeResolver = new AvataxDocumentCodeResolver();
|
|
||||||
|
|
||||||
const entityUseCode = await entityTypeMatcher.match(order.avataxEntityCode);
|
|
||||||
const date = dateResolver.resolve(order.avataxTaxCalculationDate, order.created);
|
|
||||||
const code = documentCodeResolver.resolve({
|
|
||||||
avataxDocumentCode: order.avataxDocumentCode,
|
|
||||||
orderId: order.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
model: {
|
|
||||||
type: this.matchDocumentType(avataxConfig),
|
|
||||||
entityUseCode,
|
|
||||||
code,
|
|
||||||
customerCode:
|
|
||||||
order.user?.id ??
|
|
||||||
"" /* In Saleor AvaTax plugin, the customer code is 0. In Taxes App, we set it to the user id. */,
|
|
||||||
companyCode: avataxConfig.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),
|
|
||||||
// billing or shipping address?
|
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
|
||||||
},
|
|
||||||
currencyCode: order.total.currency,
|
|
||||||
email: taxProviderUtils.resolveStringOrThrow(order.user?.email),
|
|
||||||
lines: linesTransformer.transform(order, avataxConfig, matches),
|
|
||||||
date,
|
|
||||||
discount: discountUtils.sumDiscounts(
|
|
||||||
order.discounts.map((discount) => discount.amount.amount)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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";
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedPayloadService {
|
|
||||||
constructor(private authData: AuthData) {}
|
|
||||||
|
|
||||||
private getMatches() {
|
|
||||||
const taxCodeMatchesService = new AvataxTaxCodeMatchesService(this.authData);
|
|
||||||
|
|
||||||
return taxCodeMatchesService.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPayload(
|
|
||||||
order: OrderCreatedSubscriptionFragment,
|
|
||||||
avataxConfig: AvataxConfig
|
|
||||||
): Promise<CreateTransactionArgs> {
|
|
||||||
const matches = await this.getMatches();
|
|
||||||
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer();
|
|
||||||
|
|
||||||
return payloadTransformer.transform(order, avataxConfig, matches);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,514 +0,0 @@
|
||||||
import { AdjustmentReason } from "avatax/lib/enums/AdjustmentReason";
|
|
||||||
import { BoundaryLevel } from "avatax/lib/enums/BoundaryLevel";
|
|
||||||
import { ChargedTo } from "avatax/lib/enums/ChargedTo";
|
|
||||||
import { DocumentStatus } from "avatax/lib/enums/DocumentStatus";
|
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|
||||||
import { JurisTypeId } from "avatax/lib/enums/JurisTypeId";
|
|
||||||
import { JurisdictionType } from "avatax/lib/enums/JurisdictionType";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const MOCKED_TRANSACTION: TransactionModel = {
|
|
||||||
id: 0,
|
|
||||||
code: "8fc875ce-a929-4556-9f30-0165b1597d9f",
|
|
||||||
companyId: 7799640,
|
|
||||||
date: new Date(),
|
|
||||||
paymentDate: new Date(),
|
|
||||||
status: DocumentStatus.Temporary,
|
|
||||||
type: DocumentType.SalesOrder,
|
|
||||||
batchCode: "",
|
|
||||||
currencyCode: "USD",
|
|
||||||
exchangeRateCurrencyCode: "USD",
|
|
||||||
customerUsageType: "",
|
|
||||||
entityUseCode: "",
|
|
||||||
customerVendorCode: "VXNlcjoyMDg0NTEwNDEw",
|
|
||||||
customerCode: "VXNlcjoyMDg0NTEwNDEw",
|
|
||||||
exemptNo: "",
|
|
||||||
reconciled: false,
|
|
||||||
locationCode: "",
|
|
||||||
reportingLocationCode: "",
|
|
||||||
purchaseOrderNo: "",
|
|
||||||
referenceCode: "",
|
|
||||||
salespersonCode: "",
|
|
||||||
totalAmount: 107.31,
|
|
||||||
totalExempt: 0,
|
|
||||||
totalDiscount: 0,
|
|
||||||
totalTax: 10.2,
|
|
||||||
totalTaxable: 107.31,
|
|
||||||
totalTaxCalculated: 10.2,
|
|
||||||
adjustmentReason: AdjustmentReason.NotAdjusted,
|
|
||||||
locked: false,
|
|
||||||
version: 1,
|
|
||||||
exchangeRateEffectiveDate: new Date(),
|
|
||||||
exchangeRate: 1,
|
|
||||||
modifiedDate: new Date(),
|
|
||||||
modifiedUserId: 6479978,
|
|
||||||
taxDate: new Date(),
|
|
||||||
lines: [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
lineNumber: "1",
|
|
||||||
customerUsageType: "",
|
|
||||||
entityUseCode: "",
|
|
||||||
discountAmount: 0,
|
|
||||||
exemptAmount: 0,
|
|
||||||
exemptCertId: 0,
|
|
||||||
exemptNo: "",
|
|
||||||
isItemTaxable: true,
|
|
||||||
itemCode: "",
|
|
||||||
lineAmount: 36.53,
|
|
||||||
quantity: 2,
|
|
||||||
ref1: "",
|
|
||||||
ref2: "",
|
|
||||||
reportingDate: new Date(),
|
|
||||||
tax: 3.47,
|
|
||||||
taxableAmount: 36.53,
|
|
||||||
taxCalculated: 3.47,
|
|
||||||
taxCode: "P0000000",
|
|
||||||
taxCodeId: 8087,
|
|
||||||
taxDate: new Date(),
|
|
||||||
taxIncluded: true,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "06",
|
|
||||||
jurisName: "CALIFORNIA",
|
|
||||||
stateAssignedNo: "",
|
|
||||||
jurisType: JurisTypeId.STA,
|
|
||||||
jurisdictionType: JurisdictionType.State,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.06,
|
|
||||||
tax: 2.19,
|
|
||||||
taxableAmount: 36.53,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA STATE TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 2.19,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 36.53,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 2.19,
|
|
||||||
reportingTaxCalculated: 2.19,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "037",
|
|
||||||
jurisName: "LOS ANGELES",
|
|
||||||
stateAssignedNo: "",
|
|
||||||
jurisType: JurisTypeId.CTY,
|
|
||||||
jurisdictionType: JurisdictionType.County,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.0025,
|
|
||||||
tax: 0.09,
|
|
||||||
taxableAmount: 36.53,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA COUNTY TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 0.09,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 36.53,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 0.09,
|
|
||||||
reportingTaxCalculated: 0.09,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "EMAR0",
|
|
||||||
jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP",
|
|
||||||
stateAssignedNo: "594",
|
|
||||||
jurisType: JurisTypeId.STJ,
|
|
||||||
jurisdictionType: JurisdictionType.Special,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.0225,
|
|
||||||
tax: 0.82,
|
|
||||||
taxableAmount: 36.53,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA SPECIAL TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 0.82,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 36.53,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 0.82,
|
|
||||||
reportingTaxCalculated: 0.82,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "EMTC0",
|
|
||||||
jurisName: "LOS ANGELES CO LOCAL TAX SL",
|
|
||||||
stateAssignedNo: "19",
|
|
||||||
jurisType: JurisTypeId.STJ,
|
|
||||||
jurisdictionType: JurisdictionType.Special,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.01,
|
|
||||||
tax: 0.37,
|
|
||||||
taxableAmount: 36.53,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA SPECIAL TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 0.37,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 36.53,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 0.37,
|
|
||||||
reportingTaxCalculated: 0.37,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
nonPassthroughDetails: [],
|
|
||||||
hsCode: "",
|
|
||||||
costInsuranceFreight: 0,
|
|
||||||
vatCode: "",
|
|
||||||
vatNumberTypeId: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
lineNumber: "2",
|
|
||||||
customerUsageType: "",
|
|
||||||
entityUseCode: "",
|
|
||||||
discountAmount: 0,
|
|
||||||
exemptAmount: 0,
|
|
||||||
exemptCertId: 0,
|
|
||||||
exemptNo: "",
|
|
||||||
isItemTaxable: true,
|
|
||||||
itemCode: "Shipping",
|
|
||||||
lineAmount: 70.78,
|
|
||||||
quantity: 1,
|
|
||||||
ref1: "",
|
|
||||||
ref2: "",
|
|
||||||
reportingDate: new Date(),
|
|
||||||
tax: 6.73,
|
|
||||||
taxableAmount: 70.78,
|
|
||||||
taxCalculated: 6.73,
|
|
||||||
taxCode: "P0000000",
|
|
||||||
taxCodeId: 8087,
|
|
||||||
taxDate: new Date(),
|
|
||||||
taxIncluded: true,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "06",
|
|
||||||
jurisName: "CALIFORNIA",
|
|
||||||
stateAssignedNo: "",
|
|
||||||
jurisType: JurisTypeId.STA,
|
|
||||||
jurisdictionType: JurisdictionType.State,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.06,
|
|
||||||
tax: 4.25,
|
|
||||||
taxableAmount: 70.78,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA STATE TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 4.25,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 70.78,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 4.25,
|
|
||||||
reportingTaxCalculated: 4.25,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "037",
|
|
||||||
jurisName: "LOS ANGELES",
|
|
||||||
stateAssignedNo: "",
|
|
||||||
jurisType: JurisTypeId.CTY,
|
|
||||||
jurisdictionType: JurisdictionType.County,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.0025,
|
|
||||||
tax: 0.18,
|
|
||||||
taxableAmount: 70.78,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA COUNTY TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 0.18,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 70.78,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 0.18,
|
|
||||||
reportingTaxCalculated: 0.18,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "EMAR0",
|
|
||||||
jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP",
|
|
||||||
stateAssignedNo: "594",
|
|
||||||
jurisType: JurisTypeId.STJ,
|
|
||||||
jurisdictionType: JurisdictionType.Special,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.0225,
|
|
||||||
tax: 1.59,
|
|
||||||
taxableAmount: 70.78,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA SPECIAL TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 1.59,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 70.78,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 1.59,
|
|
||||||
reportingTaxCalculated: 1.59,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionLineId: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
exemptAmount: 0,
|
|
||||||
jurisCode: "EMTC0",
|
|
||||||
jurisName: "LOS ANGELES CO LOCAL TAX SL",
|
|
||||||
stateAssignedNo: "19",
|
|
||||||
jurisType: JurisTypeId.STJ,
|
|
||||||
jurisdictionType: JurisdictionType.Special,
|
|
||||||
nonTaxableAmount: 0,
|
|
||||||
rate: 0.01,
|
|
||||||
tax: 0.71,
|
|
||||||
taxableAmount: 70.78,
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubTypeId: "U",
|
|
||||||
taxName: "CA SPECIAL TAX",
|
|
||||||
taxAuthorityTypeId: 45,
|
|
||||||
taxCalculated: 0.71,
|
|
||||||
rateType: RateType.General,
|
|
||||||
rateTypeCode: "G",
|
|
||||||
unitOfBasis: "PerCurrencyUnit",
|
|
||||||
isNonPassThru: false,
|
|
||||||
isFee: false,
|
|
||||||
reportingTaxableUnits: 70.78,
|
|
||||||
reportingNonTaxableUnits: 0,
|
|
||||||
reportingExemptUnits: 0,
|
|
||||||
reportingTax: 0.71,
|
|
||||||
reportingTaxCalculated: 0.71,
|
|
||||||
liabilityType: LiabilityType.Seller,
|
|
||||||
chargedTo: ChargedTo.Buyer,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
nonPassthroughDetails: [],
|
|
||||||
hsCode: "",
|
|
||||||
costInsuranceFreight: 0,
|
|
||||||
vatCode: "",
|
|
||||||
vatNumberTypeId: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
addresses: [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
boundaryLevel: BoundaryLevel.Zip5,
|
|
||||||
line1: "123 Palm Grove Ln",
|
|
||||||
line2: "",
|
|
||||||
line3: "",
|
|
||||||
city: "LOS ANGELES",
|
|
||||||
region: "CA",
|
|
||||||
postalCode: "90002",
|
|
||||||
country: "US",
|
|
||||||
taxRegionId: 4017056,
|
|
||||||
latitude: "33.948712",
|
|
||||||
longitude: "-118.245951",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
transactionId: 0,
|
|
||||||
boundaryLevel: BoundaryLevel.Zip5,
|
|
||||||
line1: "8559 Lake Avenue",
|
|
||||||
line2: "",
|
|
||||||
line3: "",
|
|
||||||
city: "New York",
|
|
||||||
region: "NY",
|
|
||||||
postalCode: "10001",
|
|
||||||
country: "US",
|
|
||||||
taxRegionId: 2088629,
|
|
||||||
latitude: "40.748481",
|
|
||||||
longitude: "-73.993125",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
summary: [
|
|
||||||
{
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
jurisType: JurisdictionType.State,
|
|
||||||
jurisCode: "06",
|
|
||||||
jurisName: "CALIFORNIA",
|
|
||||||
taxAuthorityType: 45,
|
|
||||||
stateAssignedNo: "",
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubType: "U",
|
|
||||||
taxName: "CA STATE TAX",
|
|
||||||
rateType: RateType.General,
|
|
||||||
taxable: 107.31,
|
|
||||||
rate: 0.06,
|
|
||||||
tax: 6.44,
|
|
||||||
taxCalculated: 6.44,
|
|
||||||
nonTaxable: 0,
|
|
||||||
exemption: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
jurisType: JurisdictionType.County,
|
|
||||||
jurisCode: "037",
|
|
||||||
jurisName: "LOS ANGELES",
|
|
||||||
taxAuthorityType: 45,
|
|
||||||
stateAssignedNo: "",
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubType: "U",
|
|
||||||
taxName: "CA COUNTY TAX",
|
|
||||||
rateType: RateType.General,
|
|
||||||
taxable: 107.31,
|
|
||||||
rate: 0.0025,
|
|
||||||
tax: 0.27,
|
|
||||||
taxCalculated: 0.27,
|
|
||||||
nonTaxable: 0,
|
|
||||||
exemption: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
jurisType: JurisdictionType.Special,
|
|
||||||
jurisCode: "EMTC0",
|
|
||||||
jurisName: "LOS ANGELES CO LOCAL TAX SL",
|
|
||||||
taxAuthorityType: 45,
|
|
||||||
stateAssignedNo: "19",
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubType: "U",
|
|
||||||
taxName: "CA SPECIAL TAX",
|
|
||||||
rateType: RateType.General,
|
|
||||||
taxable: 107.31,
|
|
||||||
rate: 0.01,
|
|
||||||
tax: 1.08,
|
|
||||||
taxCalculated: 1.08,
|
|
||||||
nonTaxable: 0,
|
|
||||||
exemption: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
country: "US",
|
|
||||||
region: "CA",
|
|
||||||
jurisType: JurisdictionType.Special,
|
|
||||||
jurisCode: "EMAR0",
|
|
||||||
jurisName: "LOS ANGELES COUNTY DISTRICT TAX SP",
|
|
||||||
taxAuthorityType: 45,
|
|
||||||
stateAssignedNo: "594",
|
|
||||||
taxType: "Use",
|
|
||||||
taxSubType: "U",
|
|
||||||
taxName: "CA SPECIAL TAX",
|
|
||||||
rateType: RateType.General,
|
|
||||||
taxable: 107.31,
|
|
||||||
rate: 0.0225,
|
|
||||||
tax: 2.41,
|
|
||||||
taxCalculated: 2.41,
|
|
||||||
nonTaxable: 0,
|
|
||||||
exemption: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("AvataxOrderCreatedResponseTransformer", () => {
|
|
||||||
it("returns orded id in response", () => {
|
|
||||||
const transformer = new AvataxOrderCreatedResponseTransformer();
|
|
||||||
const result = transformer.transform(MOCKED_TRANSACTION);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
id: "8fc875ce-a929-4556-9f30-0165b1597d9f",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("throws an error when no transaction id is present", () => {
|
|
||||||
const transformer = new AvataxOrderCreatedResponseTransformer();
|
|
||||||
|
|
||||||
expect(() => transformer.transform({} as any)).toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedResponseTransformer {
|
|
||||||
transform(response: TransactionModel): CreateOrderResponse {
|
|
||||||
return {
|
|
||||||
id: taxProviderUtils.resolveOptionalOrThrow(
|
|
||||||
response.code,
|
|
||||||
new Error(
|
|
||||||
"Could not update the order metadata with AvaTax transaction code because it was not returned from the createTransaction mutation."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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";
|
|
||||||
|
|
||||||
const mockedLine: OrderLineFragment = {
|
|
||||||
productSku: "sku",
|
|
||||||
productName: "Test product",
|
|
||||||
quantity: 1,
|
|
||||||
taxClass: {
|
|
||||||
id: "tax-class-id-2",
|
|
||||||
},
|
|
||||||
unitPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const matches: AvataxTaxCodeMatches = [
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
saleorTaxClassId: "tax-class-id",
|
|
||||||
avataxTaxCode: "P0000000",
|
|
||||||
},
|
|
||||||
id: "id-1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
saleorTaxClassId: "tax-class-id-3",
|
|
||||||
avataxTaxCode: "P0000001",
|
|
||||||
},
|
|
||||||
id: "id-2",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("AvataxOrderCreatedTaxCodeMatcher", () => {
|
|
||||||
it("should return empty string if tax class is not found", () => {
|
|
||||||
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
expect(matcher.match(line, matches)).toEqual("P0000000");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { OrderLineFragment } from "../../../../generated/graphql";
|
|
||||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedTaxCodeMatcher {
|
|
||||||
private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) {
|
|
||||||
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTaxClassId(line: OrderLineFragment): string | undefined {
|
|
||||||
return line.taxClass?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
match(line: OrderLineFragment, matches: AvataxTaxCodeMatches) {
|
|
||||||
const taxClassId = this.getTaxClassId(line);
|
|
||||||
|
|
||||||
// We can fall back to empty string if we don't have a tax code match
|
|
||||||
return taxClassId
|
|
||||||
? this.mapTaxClassWithTaxMatch(taxClassId, matches)?.data.avataxTaxCode ?? ""
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
|
||||||
import { AvataxClient, CommitTransactionArgs } from "../avatax-client";
|
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
|
||||||
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
|
|
||||||
import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer";
|
|
||||||
|
|
||||||
export type AvataxOrderFulfilledPayload = {
|
|
||||||
order: OrderFulfilledSubscriptionFragment;
|
|
||||||
};
|
|
||||||
export type AvataxOrderFulfilledTarget = CommitTransactionArgs;
|
|
||||||
export type AvataxOrderFulfilledResponse = { ok: true };
|
|
||||||
|
|
||||||
export class AvataxOrderFulfilledAdapter
|
|
||||||
implements WebhookAdapter<AvataxOrderFulfilledPayload, AvataxOrderFulfilledResponse>
|
|
||||||
{
|
|
||||||
private logger: Logger;
|
|
||||||
|
|
||||||
constructor(private readonly config: AvataxConfig) {
|
|
||||||
this.logger = createLogger({ name: "AvataxOrderFulfilledAdapter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(payload: AvataxOrderFulfilledPayload): Promise<AvataxOrderFulfilledResponse> {
|
|
||||||
this.logger.debug("Transforming the Saleor payload for commiting transaction with AvaTax...");
|
|
||||||
|
|
||||||
const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer(this.config);
|
|
||||||
const target = payloadTransformer.transform({ ...payload });
|
|
||||||
|
|
||||||
this.logger.debug("Calling AvaTax commitTransaction with transformed payload...");
|
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
|
||||||
const response = await client.commitTransaction(target);
|
|
||||||
|
|
||||||
this.logger.debug("AvaTax commitTransaction succesfully responded");
|
|
||||||
|
|
||||||
const responseTransformer = new AvataxOrderFulfilledResponseTransformer();
|
|
||||||
const transformedResponse = responseTransformer.transform(response);
|
|
||||||
|
|
||||||
this.logger.debug("Transformed AvaTax commitTransaction response");
|
|
||||||
|
|
||||||
return transformedResponse;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
|
||||||
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
|
|
||||||
|
|
||||||
// todo: add AvataxOrderFulfilledMockGenerator
|
|
||||||
|
|
||||||
const MOCK_AVATAX_CONFIG: AvataxConfig = {
|
|
||||||
companyCode: "DEFAULT",
|
|
||||||
isDocumentRecordingEnabled: true,
|
|
||||||
isAutocommit: false,
|
|
||||||
isSandbox: true,
|
|
||||||
name: "AvaTax-1",
|
|
||||||
shippingTaxCode: "FR000000",
|
|
||||||
address: {
|
|
||||||
country: "US",
|
|
||||||
zip: "10118",
|
|
||||||
state: "NY",
|
|
||||||
city: "New York",
|
|
||||||
street: "350 5th Avenue",
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
password: "password",
|
|
||||||
username: "username",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type OrderFulfilled = OrderFulfilledSubscriptionFragment;
|
|
||||||
|
|
||||||
const ORDER_FULFILLED_MOCK: OrderFulfilled = {
|
|
||||||
id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5",
|
|
||||||
created: "2023-04-11T11:03:09.304109+00:00",
|
|
||||||
avataxId: "transaction-code",
|
|
||||||
channel: {
|
|
||||||
id: "Q2hhbm5lbDoy",
|
|
||||||
slug: "channel-pln",
|
|
||||||
},
|
|
||||||
shippingAddress: {
|
|
||||||
streetAddress1: "123 Palm Grove Ln",
|
|
||||||
streetAddress2: "",
|
|
||||||
city: "LOS ANGELES",
|
|
||||||
countryArea: "CA",
|
|
||||||
postalCode: "90002",
|
|
||||||
country: {
|
|
||||||
code: "US",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
billingAddress: {
|
|
||||||
streetAddress1: "123 Palm Grove Ln",
|
|
||||||
streetAddress2: "",
|
|
||||||
city: "LOS ANGELES",
|
|
||||||
countryArea: "CA",
|
|
||||||
postalCode: "90002",
|
|
||||||
country: {
|
|
||||||
code: "US",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
total: {
|
|
||||||
net: {
|
|
||||||
amount: 183.33,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 12.83,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shippingPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 48.33,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lines: [
|
|
||||||
{
|
|
||||||
productSku: "328223581",
|
|
||||||
productName: "Monospace Tee",
|
|
||||||
quantity: 1,
|
|
||||||
unitPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 90,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 90,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 8.55,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productSku: "328223580",
|
|
||||||
productName: "Polyspace Tee",
|
|
||||||
quantity: 1,
|
|
||||||
unitPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 45,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 45,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 4.28,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const MOCKED_ORDER_FULFILLED_PAYLOAD: {
|
|
||||||
order: OrderFulfilledSubscriptionFragment;
|
|
||||||
} = {
|
|
||||||
order: ORDER_FULFILLED_MOCK,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("AvataxOrderFulfilledPayloadTransformer", () => {
|
|
||||||
it("throws error when no avataxId", () => {
|
|
||||||
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
transformer.transform({
|
|
||||||
...MOCKED_ORDER_FULFILLED_PAYLOAD,
|
|
||||||
order: {
|
|
||||||
...MOCKED_ORDER_FULFILLED_PAYLOAD.order,
|
|
||||||
avataxId: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).toThrow();
|
|
||||||
});
|
|
||||||
it("returns document type of SalesOrder when isDocumentRecordingEnabled is false", () => {
|
|
||||||
const transformer = new AvataxOrderFulfilledPayloadTransformer({
|
|
||||||
...MOCK_AVATAX_CONFIG,
|
|
||||||
isDocumentRecordingEnabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD);
|
|
||||||
|
|
||||||
expect(payload.documentType).toBe(DocumentType.SalesOrder);
|
|
||||||
}),
|
|
||||||
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", () => {
|
|
||||||
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
|
|
||||||
|
|
||||||
const payload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD);
|
|
||||||
|
|
||||||
expect(payload.documentType).toBe(DocumentType.SalesInvoice);
|
|
||||||
}),
|
|
||||||
it("returns transformed payload", () => {
|
|
||||||
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
|
|
||||||
|
|
||||||
const mappedPayload = transformer.transform(MOCKED_ORDER_FULFILLED_PAYLOAD);
|
|
||||||
|
|
||||||
expect(mappedPayload).toEqual({
|
|
||||||
transactionCode: "transaction-code",
|
|
||||||
companyCode: "DEFAULT",
|
|
||||||
documentType: DocumentType.SalesInvoice,
|
|
||||||
model: {
|
|
||||||
commit: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
|
||||||
import {
|
|
||||||
AvataxOrderFulfilledPayload,
|
|
||||||
AvataxOrderFulfilledTarget,
|
|
||||||
} from "./avatax-order-fulfilled-adapter";
|
|
||||||
|
|
||||||
export class AvataxOrderFulfilledPayloadTransformer {
|
|
||||||
constructor(private readonly config: AvataxConfig) {}
|
|
||||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
|
||||||
if (!config.isDocumentRecordingEnabled) {
|
|
||||||
return DocumentType.SalesOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DocumentType.SalesInvoice;
|
|
||||||
}
|
|
||||||
transform({ order }: AvataxOrderFulfilledPayload): AvataxOrderFulfilledTarget {
|
|
||||||
const transactionCode = z.string().min(1).parse(order.avataxId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
transactionCode,
|
|
||||||
companyCode: this.config.companyCode,
|
|
||||||
documentType: this.matchDocumentType(this.config),
|
|
||||||
model: {
|
|
||||||
commit: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
|
||||||
import { AvataxOrderFulfilledResponse } from "./avatax-order-fulfilled-adapter";
|
|
||||||
|
|
||||||
export class AvataxOrderFulfilledResponseTransformer {
|
|
||||||
transform(response: TransactionModel): AvataxOrderFulfilledResponse {
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,6 @@ import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import {
|
||||||
MetadataItem,
|
MetadataItem,
|
||||||
OrderConfirmedSubscriptionFragment,
|
OrderConfirmedSubscriptionFragment,
|
||||||
OrderCreatedSubscriptionFragment,
|
|
||||||
OrderFulfilledSubscriptionFragment,
|
|
||||||
TaxBaseFragment,
|
TaxBaseFragment,
|
||||||
} from "../../../generated/graphql";
|
} from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
|
@ -20,7 +18,10 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private client: TaxJarWebhookService | AvataxWebhookService;
|
private client: TaxJarWebhookService | AvataxWebhookService;
|
||||||
|
|
||||||
constructor(providerConnection: ProviderConnection, private authData: AuthData) {
|
constructor(
|
||||||
|
providerConnection: ProviderConnection,
|
||||||
|
private authData: AuthData,
|
||||||
|
) {
|
||||||
this.logger = createLogger({
|
this.logger = createLogger({
|
||||||
name: "ActiveTaxProviderService",
|
name: "ActiveTaxProviderService",
|
||||||
});
|
});
|
||||||
|
@ -54,29 +55,15 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
||||||
return this.client.confirmOrder(order);
|
return this.client.confirmOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
|
|
||||||
return this.client.fulfillOrder(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledPayload) {
|
async cancelOrder(payload: OrderCancelledPayload) {
|
||||||
this.client.cancelOrder(payload);
|
this.client.cancelOrder(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
async createOrder(payload: OrderCreatedSubscriptionFragment) {
|
|
||||||
return this.client.createOrder(payload);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getActiveConnectionService(
|
export function getActiveConnectionService(
|
||||||
channelSlug: string | undefined,
|
channelSlug: string | undefined,
|
||||||
encryptedMetadata: MetadataItem[],
|
encryptedMetadata: MetadataItem[],
|
||||||
authData: AuthData
|
authData: AuthData,
|
||||||
): ActiveTaxProviderService {
|
): ActiveTaxProviderService {
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
name: "getActiveConnectionService",
|
name: "getActiveConnectionService",
|
||||||
|
@ -105,13 +92,13 @@ export function getActiveConnectionService(
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerConnection = providerConnections.find(
|
const providerConnection = providerConnections.find(
|
||||||
(connection) => connection.id === channelConfig.config.providerConnectionId
|
(connection) => connection.id === channelConfig.config.providerConnectionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!providerConnection) {
|
if (!providerConnection) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ providerConnections, channelConfig },
|
{ providerConnections, channelConfig },
|
||||||
"In the providers array, there is no item with an id that matches the channel config providerConnectionId."
|
"In the providers array, there is no item with an id that matches the channel config providerConnectionId.",
|
||||||
);
|
);
|
||||||
throw new Error(`Channel config providerConnectionId does not match any providers`);
|
throw new Error(`Channel config providerConnectionId does not match any providers`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
||||||
import {
|
import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
||||||
OrderConfirmedSubscriptionFragment,
|
|
||||||
OrderCreatedSubscriptionFragment,
|
|
||||||
OrderFulfilledSubscriptionFragment,
|
|
||||||
TaxBaseFragment,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
|
|
||||||
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
||||||
|
@ -15,13 +10,4 @@ export interface ProviderWebhookService {
|
||||||
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
||||||
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||||
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
fulfillOrder: (payload: OrderFulfilledSubscriptionFragment) => Promise<{ ok: boolean }>;
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
createOrder: (payload: OrderCreatedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { describe } from "vitest";
|
|
||||||
|
|
||||||
describe.todo("TaxJarOrderCreatedAdapter", () => {});
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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";
|
|
||||||
|
|
||||||
export type TaxJarOrderCreatedPayload = {
|
|
||||||
order: OrderCreatedSubscriptionFragment;
|
|
||||||
};
|
|
||||||
export type TaxJarOrderCreatedTarget = CreateOrderArgs;
|
|
||||||
export type TaxJarOrderCreatedResponse = CreateOrderResponse;
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedAdapter
|
|
||||||
implements WebhookAdapter<TaxJarOrderCreatedPayload, TaxJarOrderCreatedResponse>
|
|
||||||
{
|
|
||||||
private logger: Logger;
|
|
||||||
constructor(private readonly config: TaxJarConfig, private authData: AuthData) {
|
|
||||||
this.logger = createLogger({ name: "TaxJarOrderCreatedAdapter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(payload: TaxJarOrderCreatedPayload): Promise<TaxJarOrderCreatedResponse> {
|
|
||||||
this.logger.debug("Transforming the Saleor payload for creating order with TaxJar...");
|
|
||||||
const payloadService = new TaxJarOrderCreatedPayloadService(this.authData);
|
|
||||||
const target = await payloadService.getPayload(payload.order, this.config);
|
|
||||||
|
|
||||||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
|
||||||
|
|
||||||
const client = new TaxJarClient(this.config);
|
|
||||||
const response = await client.createOrder(target);
|
|
||||||
|
|
||||||
this.logger.debug("TaxJar createOrder successfully responded");
|
|
||||||
const responseTransformer = new TaxJarOrderCreatedResponseTransformer();
|
|
||||||
const transformedResponse = responseTransformer.transform(response);
|
|
||||||
|
|
||||||
this.logger.debug("Transformed TaxJar createOrder response");
|
|
||||||
|
|
||||||
return transformedResponse;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
|
|
||||||
import { OrderCreatedSubscriptionFragment, OrderStatus } from "../../../../generated/graphql";
|
|
||||||
import { ChannelConfig } from "../../channel-configuration/channel-config";
|
|
||||||
import { defaultOrder } from "../../../mocks";
|
|
||||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
|
||||||
import { ChannelConfigMockGenerator } from "../../channel-configuration/channel-config-mock-generator";
|
|
||||||
|
|
||||||
type Order = OrderCreatedSubscriptionFragment;
|
|
||||||
|
|
||||||
// providerConfigMockGenerator class that other classes extend?
|
|
||||||
const defaultProviderConfig: TaxJarConfig = {
|
|
||||||
name: "taxjar-1",
|
|
||||||
credentials: {
|
|
||||||
apiKey: "test",
|
|
||||||
},
|
|
||||||
isSandbox: false,
|
|
||||||
address: {
|
|
||||||
country: "US",
|
|
||||||
zip: "95008",
|
|
||||||
state: "CA",
|
|
||||||
city: "Campbell",
|
|
||||||
street: "33 N. First Street",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultOrderCreatedResponse: CreateOrderRes = {
|
|
||||||
order: {
|
|
||||||
user_id: 314973,
|
|
||||||
transaction_reference_id: null,
|
|
||||||
transaction_id: "T3JkZXI6ZTUzZTBlM2MtMjk5Yi00OWYxLWIyZDItY2Q4NWExYTgxYjY2",
|
|
||||||
transaction_date: "2023-05-25T09:18:55.203Z",
|
|
||||||
to_zip: "94111",
|
|
||||||
to_street: "600 Montgomery St",
|
|
||||||
to_state: "CA",
|
|
||||||
to_country: "US",
|
|
||||||
to_city: "SAN FRANCISCO",
|
|
||||||
shipping: 59.17,
|
|
||||||
sales_tax: 0.0,
|
|
||||||
provider: "api",
|
|
||||||
line_items: [
|
|
||||||
{
|
|
||||||
unit_price: 20.0,
|
|
||||||
sales_tax: 5.18,
|
|
||||||
quantity: 3,
|
|
||||||
product_tax_code: "",
|
|
||||||
product_identifier: "328223580",
|
|
||||||
id: "0",
|
|
||||||
discount: 0.0,
|
|
||||||
description: "Monospace Tee",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unit_price: 20.0,
|
|
||||||
sales_tax: 1.73,
|
|
||||||
quantity: 1,
|
|
||||||
product_tax_code: "",
|
|
||||||
product_identifier: "328223581",
|
|
||||||
id: "1",
|
|
||||||
discount: 0.0,
|
|
||||||
description: "Monospace Tee",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unit_price: 50.0,
|
|
||||||
sales_tax: 8.63,
|
|
||||||
quantity: 2,
|
|
||||||
product_tax_code: "",
|
|
||||||
product_identifier: "118223581",
|
|
||||||
id: "2",
|
|
||||||
discount: 0.0,
|
|
||||||
description: "Paul's Balance 420",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
from_zip: "95008",
|
|
||||||
from_street: "33 N. First Street",
|
|
||||||
from_state: "CA",
|
|
||||||
from_country: "US",
|
|
||||||
from_city: "CAMPBELL",
|
|
||||||
exemption_type: null,
|
|
||||||
amount: 239.17,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const testingScenariosMap = {
|
|
||||||
default: {
|
|
||||||
order: defaultOrder,
|
|
||||||
response: defaultOrderCreatedResponse,
|
|
||||||
providerConfig: defaultProviderConfig,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type TestingScenario = keyof typeof testingScenariosMap;
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedMockGenerator {
|
|
||||||
constructor(private scenario: TestingScenario = "default") {}
|
|
||||||
generateOrder = (overrides: Partial<Order> = {}): Order =>
|
|
||||||
structuredClone({
|
|
||||||
...testingScenariosMap[this.scenario].order,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
generateChannelConfig = (overrides: Partial<ChannelConfig> = {}): ChannelConfig => {
|
|
||||||
const mockGenerator = new ChannelConfigMockGenerator();
|
|
||||||
|
|
||||||
return mockGenerator.generateChannelConfig(overrides);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateResponse = (overrides: Partial<CreateOrderRes> = {}): CreateOrderRes =>
|
|
||||||
structuredClone({
|
|
||||||
...testingScenariosMap[this.scenario].response,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
generateProviderConfig = (overrides: Partial<TaxJarConfig> = {}): TaxJarConfig =>
|
|
||||||
structuredClone({
|
|
||||||
...testingScenariosMap[this.scenario].providerConfig,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
import { OrderCreatedSubscriptionFragment, OrderLineFragment } 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";
|
|
||||||
|
|
||||||
const transformer = new TaxJarOrderCreatedPayloadLinesTransformer();
|
|
||||||
|
|
||||||
const mockedLines: OrderCreatedSubscriptionFragment["lines"] = [
|
|
||||||
{
|
|
||||||
productSku: "sku",
|
|
||||||
productName: "Test product",
|
|
||||||
quantity: 1,
|
|
||||||
taxClass: {
|
|
||||||
id: "tax-class-id-2",
|
|
||||||
},
|
|
||||||
unitPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productSku: "sku-2",
|
|
||||||
productName: "Test product 2",
|
|
||||||
quantity: 2,
|
|
||||||
taxClass: {
|
|
||||||
id: "tax-class-id-3",
|
|
||||||
},
|
|
||||||
unitPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 15,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 30,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const matches: TaxJarTaxCodeMatches = [
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
saleorTaxClassId: "tax-class-id",
|
|
||||||
taxJarTaxCode: "P0000000",
|
|
||||||
},
|
|
||||||
id: "id-1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
saleorTaxClassId: "tax-class-id-3",
|
|
||||||
taxJarTaxCode: "P0000001",
|
|
||||||
},
|
|
||||||
id: "id-2",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("TaxJarOrderCreatedPayloadLinesTransformer", () => {
|
|
||||||
it("should map payload lines correctly", () => {
|
|
||||||
expect(transformer.transform(mockedLines, matches)).toEqual([
|
|
||||||
{
|
|
||||||
quantity: 1,
|
|
||||||
unit_price: 10,
|
|
||||||
product_identifier: "sku",
|
|
||||||
product_tax_code: "",
|
|
||||||
sales_tax: 1,
|
|
||||||
description: "Test product",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
quantity: 2,
|
|
||||||
unit_price: 15,
|
|
||||||
product_identifier: "sku-2",
|
|
||||||
product_tax_code: "P0000001",
|
|
||||||
sales_tax: 3,
|
|
||||||
description: "Test product 2",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { LineItem } from "taxjar/dist/util/types";
|
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
|
||||||
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
|
||||||
import { TaxJarOrderCreatedTaxCodeMatcher } from "./taxjar-order-created-tax-code-matcher";
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedPayloadLinesTransformer {
|
|
||||||
transform(
|
|
||||||
lines: OrderCreatedSubscriptionFragment["lines"],
|
|
||||||
matches: TaxJarTaxCodeMatches
|
|
||||||
): LineItem[] {
|
|
||||||
return lines.map((line) => {
|
|
||||||
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
|
||||||
const taxCode = matcher.match(line, matches);
|
|
||||||
|
|
||||||
return {
|
|
||||||
quantity: line.quantity,
|
|
||||||
unit_price: line.unitPrice.net.amount,
|
|
||||||
product_identifier: line.productSku ?? "",
|
|
||||||
product_tax_code: taxCode,
|
|
||||||
sales_tax: line.totalPrice.tax.amount,
|
|
||||||
description: line.productName,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { TaxJarOrderCreatedMockGenerator } from "./taxjar-order-created-mock-generator";
|
|
||||||
import {
|
|
||||||
TaxJarOrderCreatedPayloadTransformer,
|
|
||||||
sumPayloadLines,
|
|
||||||
} from "./taxjar-order-created-payload-transformer";
|
|
||||||
|
|
||||||
const mockGenerator = new TaxJarOrderCreatedMockGenerator();
|
|
||||||
|
|
||||||
describe("TaxJarOrderCreatedPayloadTransformer", () => {
|
|
||||||
it("returns the correct order amount", () => {
|
|
||||||
const orderMock = mockGenerator.generateOrder();
|
|
||||||
const providerConfig = mockGenerator.generateProviderConfig();
|
|
||||||
const transformer = new TaxJarOrderCreatedPayloadTransformer();
|
|
||||||
const transformedPayload = transformer.transform(orderMock, providerConfig, []);
|
|
||||||
|
|
||||||
expect(transformedPayload.params.amount).toBe(239.17);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sumPayloadLines", () => {
|
|
||||||
it("returns the sum of all line items when items quantity = 1", () => {
|
|
||||||
const result = sumPayloadLines([
|
|
||||||
{
|
|
||||||
quantity: 1,
|
|
||||||
unit_price: 90.45,
|
|
||||||
product_identifier: "328223581",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
quantity: 1,
|
|
||||||
unit_price: 45.25,
|
|
||||||
product_identifier: "328223580",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(result).toBe(135.7);
|
|
||||||
});
|
|
||||||
it("returns the sum of all line items when items quantity > 1", () => {
|
|
||||||
const result = sumPayloadLines([
|
|
||||||
{
|
|
||||||
quantity: 3,
|
|
||||||
unit_price: 90.45,
|
|
||||||
product_identifier: "328223581",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
quantity: 2,
|
|
||||||
unit_price: 45.25,
|
|
||||||
product_identifier: "328223580",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
quantity: 1,
|
|
||||||
unit_price: 50.25,
|
|
||||||
product_identifier: "328223580",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(result).toBe(412.1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { LineItem } from "taxjar/dist/util/types";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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";
|
|
||||||
|
|
||||||
export function sumPayloadLines(lines: LineItem[]): number {
|
|
||||||
return numbers.roundFloatToTwoDecimals(
|
|
||||||
lines.reduce(
|
|
||||||
(prev, line) =>
|
|
||||||
prev +
|
|
||||||
taxProviderUtils.resolveOptionalOrThrow(
|
|
||||||
line.unit_price,
|
|
||||||
new Error("Line unit_price is required to calculate order taxes")
|
|
||||||
) *
|
|
||||||
taxProviderUtils.resolveOptionalOrThrow(
|
|
||||||
line.quantity,
|
|
||||||
new Error("Line quantity is required to calculate order taxes")
|
|
||||||
),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedPayloadTransformer {
|
|
||||||
transform(
|
|
||||||
order: OrderCreatedSubscriptionFragment,
|
|
||||||
taxJarConfig: TaxJarConfig,
|
|
||||||
matches: TaxJarTaxCodeMatches
|
|
||||||
): TaxJarOrderCreatedTarget {
|
|
||||||
const linesTransformer = new TaxJarOrderCreatedPayloadLinesTransformer();
|
|
||||||
const lineItems = linesTransformer.transform(order.lines, matches);
|
|
||||||
const lineSum = sumPayloadLines(lineItems);
|
|
||||||
const shippingAmount = order.shippingPrice.gross.amount;
|
|
||||||
/**
|
|
||||||
* "The TaxJar API performs arbitrary-precision decimal arithmetic for accurately calculating sales tax."
|
|
||||||
* but we want to round to 2 decimals for consistency
|
|
||||||
*/
|
|
||||||
const orderAmount = numbers.roundFloatToTwoDecimals(shippingAmount + lineSum);
|
|
||||||
|
|
||||||
return {
|
|
||||||
params: {
|
|
||||||
from_country: taxJarConfig.address.country,
|
|
||||||
from_zip: taxJarConfig.address.zip,
|
|
||||||
from_state: taxJarConfig.address.state,
|
|
||||||
from_city: taxJarConfig.address.city,
|
|
||||||
from_street: taxJarConfig.address.street,
|
|
||||||
to_country: order.shippingAddress!.country.code,
|
|
||||||
to_zip: order.shippingAddress!.postalCode,
|
|
||||||
to_state: order.shippingAddress!.countryArea,
|
|
||||||
to_city: order.shippingAddress!.city,
|
|
||||||
to_street: `${order.shippingAddress!.streetAddress1} ${
|
|
||||||
order.shippingAddress!.streetAddress2
|
|
||||||
}`,
|
|
||||||
shipping: shippingAmount,
|
|
||||||
line_items: lineItems,
|
|
||||||
transaction_date: order.created,
|
|
||||||
transaction_id: order.id,
|
|
||||||
amount: orderAmount,
|
|
||||||
|
|
||||||
// todo: add sales_tax
|
|
||||||
sales_tax: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { OrderCreatedSubscriptionFragment } 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 { CreateOrderArgs } from "../taxjar-client";
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedPayloadService {
|
|
||||||
constructor(private authData: AuthData) {}
|
|
||||||
|
|
||||||
private getMatches() {
|
|
||||||
const taxCodeMatchesService = new TaxJarTaxCodeMatchesService(this.authData);
|
|
||||||
|
|
||||||
return taxCodeMatchesService.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPayload(
|
|
||||||
order: OrderCreatedSubscriptionFragment,
|
|
||||||
taxJarConfig: TaxJarConfig
|
|
||||||
): Promise<CreateOrderArgs> {
|
|
||||||
const matches = await this.getMatches();
|
|
||||||
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer();
|
|
||||||
|
|
||||||
return payloadTransformer.transform(order, taxJarConfig, matches);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 +0,0 @@
|
||||||
import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
|
|
||||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedResponseTransformer {
|
|
||||||
transform(response: CreateOrderRes): CreateOrderResponse {
|
|
||||||
return {
|
|
||||||
id: response.order.transaction_id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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 { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
const mockedLine: OrderLineFragment = {
|
|
||||||
productSku: "sku",
|
|
||||||
productName: "Test product",
|
|
||||||
quantity: 1,
|
|
||||||
taxClass: {
|
|
||||||
id: "tax-class-id-2",
|
|
||||||
},
|
|
||||||
unitPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPrice: {
|
|
||||||
net: {
|
|
||||||
amount: 10,
|
|
||||||
},
|
|
||||||
tax: {
|
|
||||||
amount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const matches: TaxJarTaxCodeMatches = [
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
saleorTaxClassId: "tax-class-id",
|
|
||||||
taxJarTaxCode: "P0000000",
|
|
||||||
},
|
|
||||||
id: "id-1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
saleorTaxClassId: "tax-class-id-3",
|
|
||||||
taxJarTaxCode: "P0000001",
|
|
||||||
},
|
|
||||||
id: "id-2",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("TaxJarOrderCreatedTaxCodeMatcher", () => {
|
|
||||||
it("should return empty string if tax class is not found", () => {
|
|
||||||
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
expect(matcher.match(line, matches)).toEqual("P0000000");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { OrderLineFragment } from "../../../../generated/graphql";
|
|
||||||
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
|
||||||
|
|
||||||
export class TaxJarOrderCreatedTaxCodeMatcher {
|
|
||||||
private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) {
|
|
||||||
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTaxClassId(line: OrderLineFragment): string | undefined {
|
|
||||||
return line.taxClass?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
match(line: OrderLineFragment, matches: TaxJarTaxCodeMatches) {
|
|
||||||
const taxClassId = this.getTaxClassId(line);
|
|
||||||
|
|
||||||
// We can fall back to empty string if we don't have a tax code match
|
|
||||||
return taxClassId
|
|
||||||
? this.mapTaxClassWithTaxMatch(taxClassId, matches)?.data.taxJarTaxCode ?? ""
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,14 +2,12 @@ import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import {
|
||||||
OrderCancelledEventSubscriptionFragment,
|
OrderCancelledEventSubscriptionFragment,
|
||||||
OrderConfirmedSubscriptionFragment,
|
OrderConfirmedSubscriptionFragment,
|
||||||
OrderCreatedSubscriptionFragment,
|
|
||||||
TaxBaseFragment,
|
TaxBaseFragment,
|
||||||
} from "../../../generated/graphql";
|
} from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
||||||
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
||||||
import { TaxJarOrderCreatedAdapter } from "./order-created/taxjar-order-created-adapter";
|
|
||||||
import { TaxJarClient } from "./taxjar-client";
|
import { TaxJarClient } from "./taxjar-client";
|
||||||
import { TaxJarConfig } from "./taxjar-connection-schema";
|
import { TaxJarConfig } from "./taxjar-connection-schema";
|
||||||
|
|
||||||
|
@ -18,7 +16,10 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private config: TaxJarConfig;
|
private config: TaxJarConfig;
|
||||||
|
|
||||||
constructor(config: TaxJarConfig, private authData: AuthData) {
|
constructor(
|
||||||
|
config: TaxJarConfig,
|
||||||
|
private authData: AuthData,
|
||||||
|
) {
|
||||||
const taxJarClient = new TaxJarClient(config);
|
const taxJarClient = new TaxJarClient(config);
|
||||||
|
|
||||||
this.client = taxJarClient;
|
this.client = taxJarClient;
|
||||||
|
@ -44,24 +45,6 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
async createOrder(payload: OrderCreatedSubscriptionFragment) {
|
|
||||||
const adapter = new TaxJarOrderCreatedAdapter(this.config, this.authData);
|
|
||||||
|
|
||||||
const response = await adapter.send({ order: payload });
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This method is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
async fulfillOrder() {
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
|
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
|
||||||
// TaxJar isn't implemented yet
|
// TaxJar isn't implemented yet
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import { orderCalculateTaxesSyncWebhook } from "./webhooks/order-calculate-taxes
|
||||||
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
|
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
|
||||||
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
|
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
|
||||||
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
|
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
|
||||||
import { orderFulfilledAsyncWebhook } from "./webhooks/order-fulfilled";
|
|
||||||
import { orderCreatedAsyncWebhook } from "./webhooks/order-created";
|
|
||||||
|
|
||||||
export default createManifestHandler({
|
export default createManifestHandler({
|
||||||
async manifestFactory({ appBaseUrl }) {
|
async manifestFactory({ appBaseUrl }) {
|
||||||
|
@ -39,8 +37,6 @@ export default createManifestHandler({
|
||||||
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderFulfilledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
|
||||||
orderCreatedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
|
||||||
import {
|
|
||||||
OrderCreatedEventSubscriptionFragment,
|
|
||||||
OrderStatus,
|
|
||||||
UntypedOrderCreatedSubscriptionDocument,
|
|
||||||
} from "../../../../generated/graphql";
|
|
||||||
import { saleorApp } from "../../../../saleor-app";
|
|
||||||
import { createLogger } from "../../../lib/logger";
|
|
||||||
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
|
|
||||||
import { WebhookResponse } from "../../../modules/app/webhook-response";
|
|
||||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
|
||||||
import { OrderMetadataManager } from "../../../modules/app/order-metadata-manager";
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
api: {
|
|
||||||
bodyParser: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type OrderCreatedPayload = Extract<
|
|
||||||
OrderCreatedEventSubscriptionFragment,
|
|
||||||
{ __typename: "OrderCreated" }
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This handler is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
export const orderCreatedAsyncWebhook = new SaleorAsyncWebhook<OrderCreatedPayload>({
|
|
||||||
name: "OrderCreated",
|
|
||||||
apl: saleorApp.apl,
|
|
||||||
event: "ORDER_CREATED",
|
|
||||||
query: UntypedOrderCreatedSubscriptionDocument,
|
|
||||||
webhookPath: "/api/webhooks/order-created",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => {
|
|
||||||
const logger = createLogger({ event: ctx.event });
|
|
||||||
const { payload, authData } = ctx;
|
|
||||||
const { saleorApiUrl, token } = authData;
|
|
||||||
const webhookResponse = new WebhookResponse(res);
|
|
||||||
|
|
||||||
logger.info("Handler called with payload");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
|
||||||
const channelSlug = payload.order?.channel.slug;
|
|
||||||
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
|
|
||||||
|
|
||||||
// todo: figure out what fields are needed and add validation
|
|
||||||
if (!payload.order) {
|
|
||||||
return webhookResponse.error(new Error("Insufficient order data"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.order.status === OrderStatus.Fulfilled) {
|
|
||||||
return webhookResponse.error(new Error("Skipping fulfilled order to prevent duplication"));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Creating order...");
|
|
||||||
|
|
||||||
const createdOrder = await taxProvider.createOrder(payload.order);
|
|
||||||
|
|
||||||
logger.info({ createdOrder }, "Order created");
|
|
||||||
const client = createGraphQLClient({
|
|
||||||
saleorApiUrl,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const orderMetadataManager = new OrderMetadataManager(client);
|
|
||||||
|
|
||||||
await orderMetadataManager.updateOrderMetadataWithExternalId(payload.order.id, createdOrder.id);
|
|
||||||
logger.info("Updated order metadata with externalId");
|
|
||||||
|
|
||||||
return webhookResponse.success();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error });
|
|
||||||
return webhookResponse.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
|
||||||
import {
|
|
||||||
OrderFulfilledEventSubscriptionFragment,
|
|
||||||
UntypedOrderFulfilledSubscriptionDocument,
|
|
||||||
} from "../../../../generated/graphql";
|
|
||||||
import { saleorApp } from "../../../../saleor-app";
|
|
||||||
import { createLogger } from "../../../lib/logger";
|
|
||||||
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
|
|
||||||
import { WebhookResponse } from "../../../modules/app/webhook-response";
|
|
||||||
export const config = {
|
|
||||||
api: {
|
|
||||||
bodyParser: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type OrderFulfilledPayload = Extract<
|
|
||||||
OrderFulfilledEventSubscriptionFragment,
|
|
||||||
{ __typename: "OrderFulfilled" }
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This handler is deprecated and will be removed in the future.
|
|
||||||
*/
|
|
||||||
export const orderFulfilledAsyncWebhook = new SaleorAsyncWebhook<OrderFulfilledPayload>({
|
|
||||||
name: "OrderFulfilled",
|
|
||||||
apl: saleorApp.apl,
|
|
||||||
event: "ORDER_FULFILLED",
|
|
||||||
query: UntypedOrderFulfilledSubscriptionDocument,
|
|
||||||
webhookPath: "/api/webhooks/order-fulfilled",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default orderFulfilledAsyncWebhook.createHandler(async (req, res, ctx) => {
|
|
||||||
const logger = createLogger({ event: ctx.event });
|
|
||||||
const { payload } = ctx;
|
|
||||||
const webhookResponse = new WebhookResponse(res);
|
|
||||||
|
|
||||||
logger.info("Handler called with payload");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
|
||||||
const channelSlug = payload.order?.channel.slug;
|
|
||||||
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
|
|
||||||
|
|
||||||
// todo: figure out what fields are needed and add validation
|
|
||||||
if (!payload.order) {
|
|
||||||
return webhookResponse.error(new Error("Insufficient order data"));
|
|
||||||
}
|
|
||||||
logger.info("Fulfilling order...");
|
|
||||||
|
|
||||||
await taxProvider.fulfillOrder(payload.order);
|
|
||||||
|
|
||||||
logger.info("Order fulfilled");
|
|
||||||
|
|
||||||
return webhookResponse.success();
|
|
||||||
} catch (error) {
|
|
||||||
return webhookResponse.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
Loading…
Reference in a new issue