Complete migration to OrderConfirmed flow (#916)

* refactor: 🔥 order-created & order-fulfilled code

* build: 👷 add changeset

* fix: 🐛 fix build
This commit is contained in:
Adrian Pilarczyk 2023-08-24 10:32:31 +02:00 committed by GitHub
parent 3bd2d33d97
commit 783bd5ec55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 22 additions and 3406 deletions

View 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.

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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;
}
} }

View file

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

View file

@ -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;
}
}

View file

@ -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,
});
}

View file

@ -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,
});
});
});

View file

@ -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;
}
}

View file

@ -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);
});
});

View file

@ -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)
),
},
};
}
}

View file

@ -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);
}
}

View file

@ -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();
});
});

View file

@ -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."
)
),
};
}
}

View file

@ -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");
});
});

View file

@ -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 ?? ""
: "";
}
}

View file

@ -1,44 +0,0 @@
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
import { Logger, createLogger } from "../../../lib/logger";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient, CommitTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer";
export type AvataxOrderFulfilledPayload = {
order: OrderFulfilledSubscriptionFragment;
};
export type AvataxOrderFulfilledTarget = CommitTransactionArgs;
export type AvataxOrderFulfilledResponse = { ok: true };
export class AvataxOrderFulfilledAdapter
implements WebhookAdapter<AvataxOrderFulfilledPayload, AvataxOrderFulfilledResponse>
{
private logger: Logger;
constructor(private readonly config: AvataxConfig) {
this.logger = createLogger({ name: "AvataxOrderFulfilledAdapter" });
}
async send(payload: AvataxOrderFulfilledPayload): Promise<AvataxOrderFulfilledResponse> {
this.logger.debug("Transforming the Saleor payload for commiting transaction with AvaTax...");
const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer(this.config);
const target = payloadTransformer.transform({ ...payload });
this.logger.debug("Calling AvaTax commitTransaction with transformed payload...");
const client = new AvataxClient(this.config);
const response = await client.commitTransaction(target);
this.logger.debug("AvaTax commitTransaction succesfully responded");
const responseTransformer = new AvataxOrderFulfilledResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed AvaTax commitTransaction response");
return transformedResponse;
}
}

View file

@ -1,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,
},
});
});
});

View file

@ -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,
},
};
}
}

View file

@ -1,8 +0,0 @@
import { TransactionModel } from "avatax/lib/models/TransactionModel";
import { AvataxOrderFulfilledResponse } from "./avatax-order-fulfilled-adapter";
export class AvataxOrderFulfilledResponseTransformer {
transform(response: TransactionModel): AvataxOrderFulfilledResponse {
return { ok: true };
}
}

View file

@ -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`);
} }

View file

@ -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>;
} }

View file

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

View file

@ -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;
}
}

View file

@ -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,
});
}

View file

@ -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",
},
]);
});
});

View file

@ -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,
};
});
}
}

View file

@ -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);
});
});

View file

@ -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,
},
};
}
}

View file

@ -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);
}
}

View file

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

View file

@ -1,10 +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,
};
}
}

View file

@ -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");
});
});

View file

@ -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 ?? ""
: "";
}
}

View file

@ -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
} }

View file

@ -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),
], ],
}; };

View file

@ -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);
}
});

View file

@ -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);
}
});