Compare commits
6 commits
main
...
taxes/refu
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c302f41f3e | ||
![]() |
2f8051bacc | ||
![]() |
5f82d274fd | ||
![]() |
542bdcaa84 | ||
![]() |
6e084a67e4 | ||
![]() |
f47481a74e |
27 changed files with 6267 additions and 2264 deletions
5
apps/taxes/graphql/fragments/AvataxOrderMetadata.graphql
Normal file
5
apps/taxes/graphql/fragments/AvataxOrderMetadata.graphql
Normal file
|
@ -0,0 +1,5 @@
|
|||
fragment AvataxOrderMetadata on Order {
|
||||
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||
avataxTaxCalculationDate: metafield(key: "avataxTaxCalculationDate")
|
||||
avataxDocumentCode: metafield(key: "avataxDocumentCode")
|
||||
}
|
4
apps/taxes/graphql/fragments/Money.graphql
Normal file
4
apps/taxes/graphql/fragments/Money.graphql
Normal file
|
@ -0,0 +1,4 @@
|
|||
fragment Money on Money {
|
||||
currency
|
||||
amount
|
||||
}
|
22
apps/taxes/graphql/fragments/OrderLine.graphql
Normal file
22
apps/taxes/graphql/fragments/OrderLine.graphql
Normal file
|
@ -0,0 +1,22 @@
|
|||
fragment OrderLine on OrderLine {
|
||||
id
|
||||
productSku
|
||||
productName
|
||||
quantity
|
||||
taxClass {
|
||||
id
|
||||
}
|
||||
unitPrice {
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
totalPrice {
|
||||
net {
|
||||
amount
|
||||
}
|
||||
tax {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,25 +1,3 @@
|
|||
fragment OrderLine on OrderLine {
|
||||
productSku
|
||||
productName
|
||||
quantity
|
||||
taxClass {
|
||||
id
|
||||
}
|
||||
unitPrice {
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
totalPrice {
|
||||
net {
|
||||
amount
|
||||
}
|
||||
tax {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment OrderConfirmedSubscription on Order {
|
||||
id
|
||||
number
|
||||
|
|
43
apps/taxes/graphql/subscriptions/OrderRefunded.graphql
Normal file
43
apps/taxes/graphql/subscriptions/OrderRefunded.graphql
Normal file
|
@ -0,0 +1,43 @@
|
|||
fragment OrderRefundedSubscription on Order {
|
||||
id
|
||||
avataxId: metafield(key: "avataxId")
|
||||
...AvataxOrderMetadata
|
||||
user {
|
||||
id
|
||||
email
|
||||
}
|
||||
channel {
|
||||
id
|
||||
slug
|
||||
}
|
||||
transactions {
|
||||
id
|
||||
refundedAmount {
|
||||
...Money
|
||||
}
|
||||
}
|
||||
shippingAddress {
|
||||
...Address
|
||||
}
|
||||
}
|
||||
|
||||
fragment OrderRefundedEventSubscription on Event {
|
||||
__typename
|
||||
... on OrderRefunded {
|
||||
order {
|
||||
...OrderRefundedSubscription
|
||||
}
|
||||
recipient {
|
||||
privateMetadata {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription OrderRefundedSubscription {
|
||||
event {
|
||||
...OrderRefundedEventSubscription
|
||||
}
|
||||
}
|
|
@ -65,6 +65,6 @@
|
|||
},
|
||||
"private": true,
|
||||
"saleor": {
|
||||
"schemaVersion": "3.10"
|
||||
"schemaVersion": "3.14"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = {
|
|||
},
|
||||
lines: [
|
||||
{
|
||||
id: "T3JkZXJMaW5lOjE=",
|
||||
productSku: "328223580",
|
||||
productName: "Monospace Tee",
|
||||
quantity: 3,
|
||||
|
@ -71,6 +72,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = {
|
|||
},
|
||||
},
|
||||
{
|
||||
id: "T3JkZXJMaW5lOjI=",
|
||||
productSku: "328223581",
|
||||
productName: "Monospace Tee",
|
||||
quantity: 1,
|
||||
|
@ -89,6 +91,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = {
|
|||
},
|
||||
},
|
||||
{
|
||||
id: "T3JkZXJMaW5lOjM=",
|
||||
productSku: "118223581",
|
||||
productName: "Paul's Balance 420",
|
||||
quantity: 2,
|
||||
|
|
|
@ -55,6 +55,11 @@ export type VoidTransactionArgs = {
|
|||
companyCode: string;
|
||||
};
|
||||
|
||||
export type RefundTransactionParams = Pick<
|
||||
CreateTransactionModel,
|
||||
"customerCode" | "lines" | "date" | "addresses" | "code" | "companyCode"
|
||||
>;
|
||||
|
||||
export class AvataxClient {
|
||||
private client: Avatax;
|
||||
|
||||
|
@ -112,4 +117,17 @@ export class AvataxClient {
|
|||
filter: `code eq ${useCode}`,
|
||||
});
|
||||
}
|
||||
|
||||
async refundTransaction(params: RefundTransactionParams) {
|
||||
// https://developer.avalara.com/erp-integration-guide/refunds-badge/refunds-with-create-transactions/
|
||||
return this.client.createOrAdjustTransaction({
|
||||
model: {
|
||||
createTransactionModel: {
|
||||
type: DocumentType.ReturnInvoice,
|
||||
commit: true,
|
||||
...params,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ export class AvataxDocumentCodeResolver {
|
|||
orderId,
|
||||
}: {
|
||||
avataxDocumentCode: string | null | undefined;
|
||||
orderId: string;
|
||||
orderId: string | undefined;
|
||||
}): string {
|
||||
/*
|
||||
* The value for "code" can be provided in the metadata.
|
||||
|
@ -14,9 +14,12 @@ export class AvataxDocumentCodeResolver {
|
|||
|
||||
const code = avataxDocumentCode ?? orderId;
|
||||
|
||||
if (!code) {
|
||||
throw new Error("Order id or document code must be provided");
|
||||
}
|
||||
|
||||
/*
|
||||
* The requirement from AvaTax API is that document code is a string that must be between 1 and 20 characters long.
|
||||
* // todo: document that its sliced
|
||||
*/
|
||||
return code.slice(0, 20);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../lib/logger";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||
import { AvataxClient } from "./avatax-client";
|
||||
import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
|
||||
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
||||
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
||||
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { AvataxOrderRefundedAdapter } from "./order-refunded/avatax-order-refunded-adapter";
|
||||
|
||||
export class AvataxWebhookService implements ProviderWebhookService {
|
||||
config = defaultAvataxConfig;
|
||||
|
@ -49,4 +51,10 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
|||
|
||||
await adapter.send(payload);
|
||||
}
|
||||
|
||||
async refundOrder(payload: OrderRefundedPayload) {
|
||||
const adapter = new AvataxOrderRefundedAdapter(this.config);
|
||||
|
||||
return adapter.send(payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-
|
|||
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { AvataxAddressResolver } from "../order-confirmed/avatax-address-resolver";
|
||||
|
||||
export class AvataxCalculateTaxesPayloadTransformer {
|
||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
||||
|
@ -47,6 +48,11 @@ export class AvataxCalculateTaxesPayloadTransformer {
|
|||
);
|
||||
|
||||
const customerCode = this.resolveCustomerCode(payload);
|
||||
const addressResolver = new AvataxAddressResolver();
|
||||
const addresses = addressResolver.resolve({
|
||||
from: avataxConfig.address,
|
||||
to: payload.taxBase.address!,
|
||||
});
|
||||
|
||||
return {
|
||||
model: {
|
||||
|
@ -56,10 +62,7 @@ export class AvataxCalculateTaxesPayloadTransformer {
|
|||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||
commit: avataxConfig.isAutocommit,
|
||||
addresses: {
|
||||
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
|
||||
shipTo: avataxAddressFactory.fromSaleorAddress(payload.taxBase.address!),
|
||||
},
|
||||
addresses,
|
||||
currencyCode: payload.taxBase.currency,
|
||||
lines: payloadLinesTransformer.transform(payload.taxBase, avataxConfig, matches),
|
||||
date: new Date(),
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { AddressesModel } from "avatax/lib/models/AddressesModel";
|
||||
import { AddressFragment } from "../../../../generated/graphql";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
import { avataxAddressFactory } from "../address-factory";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
|
||||
export class AvataxAddressResolver {
|
||||
resolve({
|
||||
from,
|
||||
to,
|
||||
}: {
|
||||
from: AvataxConfig["address"];
|
||||
to: AddressFragment | undefined | null;
|
||||
}): AddressesModel {
|
||||
return {
|
||||
shipFrom: avataxAddressFactory.fromChannelAddress(from),
|
||||
shipTo: avataxAddressFactory.fromSaleorAddress(taxProviderUtils.resolveOptionalOrThrow(to)),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||
import { discountUtils } from "../../taxes/discount-utils";
|
||||
import { avataxAddressFactory } from "../address-factory";
|
||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||
|
@ -10,6 +9,7 @@ import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
|||
import { AvataxDocumentCodeResolver } from "../avatax-document-code-resolver";
|
||||
import { AvataxCalculationDateResolver } from "../avatax-calculation-date-resolver";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
import { AvataxAddressResolver } from "./avatax-address-resolver";
|
||||
|
||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||
|
||||
|
@ -41,6 +41,12 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
|||
orderId: order.id,
|
||||
});
|
||||
|
||||
const addressResolver = new AvataxAddressResolver();
|
||||
const addresses = addressResolver.resolve({
|
||||
from: avataxConfig.address,
|
||||
to: order.shippingAddress!,
|
||||
});
|
||||
|
||||
return {
|
||||
model: {
|
||||
code,
|
||||
|
@ -50,11 +56,7 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
|||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||
commit: avataxConfig.isAutocommit,
|
||||
addresses: {
|
||||
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
|
||||
// billing or shipping address?
|
||||
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
||||
},
|
||||
addresses,
|
||||
currencyCode: order.total.currency,
|
||||
email: taxProviderUtils.resolveStringOrThrow(order.user?.email),
|
||||
lines: linesTransformer.transform(order, avataxConfig, matches),
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-reposito
|
|||
import { AvataxOrderConfirmedTaxCodeMatcher } from "./avatax-order-confirmed-tax-code-matcher";
|
||||
|
||||
const mockedLine: OrderLineFragment = {
|
||||
id: "T3JkZXJMaW5lOjE=",
|
||||
productSku: "sku",
|
||||
productName: "Test product",
|
||||
quantity: 1,
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { Logger, createLogger } from "../../../lib/logger";
|
||||
import { OrderRefundedPayload } from "../../../pages/api/webhooks/order-refunded";
|
||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||
import { AvataxClient } from "../avatax-client";
|
||||
import { AvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxOrderRefundedPayloadTransformer } from "./avatax-order-refunded-payload-transformer";
|
||||
|
||||
export class AvataxOrderRefundedAdapter implements WebhookAdapter<OrderRefundedPayload, void> {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(private readonly config: AvataxConfig) {
|
||||
this.logger = createLogger({ name: "AvataxOrderRefundedAdapter" });
|
||||
}
|
||||
|
||||
async send(payload: OrderRefundedPayload) {
|
||||
this.logger.debug(
|
||||
{ payload },
|
||||
"Transforming the Saleor payload for refunding order with AvaTax...",
|
||||
);
|
||||
|
||||
if (!this.config.isAutocommit) {
|
||||
throw new Error(
|
||||
"Unable to refund transaction. AvaTax can only refund commited transactions.",
|
||||
);
|
||||
}
|
||||
|
||||
const client = new AvataxClient(this.config);
|
||||
const payloadTransformer = new AvataxOrderRefundedPayloadTransformer();
|
||||
const target = payloadTransformer.transform(payload, this.config);
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
target,
|
||||
},
|
||||
`Refunding the transaction...`,
|
||||
);
|
||||
|
||||
const response = await client.refundTransaction(target);
|
||||
|
||||
this.logger.debug({ response }, `Succesfully refunded the transaction`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { OrderRefundedPayload } from "../../../pages/api/webhooks/order-refunded";
|
||||
import { RefundTransactionParams } from "../avatax-client";
|
||||
|
||||
export class AvataxOrderRefundedLinesTransformer {
|
||||
transform(payload: OrderRefundedPayload): RefundTransactionParams["lines"] {
|
||||
const refundTransactions =
|
||||
payload.order?.transactions.filter((t) => t.refundedAmount.amount > 0) ?? [];
|
||||
|
||||
if (!refundTransactions.length) {
|
||||
throw new Error("Cannot refund order without any refund transactions");
|
||||
}
|
||||
|
||||
return refundTransactions.map((t) => ({
|
||||
amount: -t.refundedAmount.amount,
|
||||
taxIncluded: true,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { Logger, createLogger } from "../../../lib/logger";
|
||||
import { OrderRefundedPayload } from "../../../pages/api/webhooks/order-refunded";
|
||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||
import { RefundTransactionParams } from "../avatax-client";
|
||||
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
||||
import { AvataxDocumentCodeResolver } from "../avatax-document-code-resolver";
|
||||
import { AvataxAddressResolver } from "../order-confirmed/avatax-address-resolver";
|
||||
import { AvataxOrderRefundedLinesTransformer } from "./avatax-order-refunded-lines-transformer";
|
||||
|
||||
export class AvataxOrderRefundedPayloadTransformer {
|
||||
private logger: Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = createLogger({ name: "AvataxOrderRefundedPayloadTransformer" });
|
||||
}
|
||||
|
||||
transform(payload: OrderRefundedPayload, avataxConfig: AvataxConfig): RefundTransactionParams {
|
||||
this.logger.debug(
|
||||
{ payload },
|
||||
"Transforming the Saleor payload for refunding order with AvaTax...",
|
||||
);
|
||||
|
||||
const addressResolver = new AvataxAddressResolver();
|
||||
const linesTransformer = new AvataxOrderRefundedLinesTransformer();
|
||||
const documentCodeResolver = new AvataxDocumentCodeResolver();
|
||||
|
||||
const addresses = addressResolver.resolve({
|
||||
from: avataxConfig.address,
|
||||
to: payload.order?.shippingAddress,
|
||||
});
|
||||
const lines = linesTransformer.transform(payload);
|
||||
const customerCode = taxProviderUtils.resolveStringOrThrow(payload.order?.user?.id);
|
||||
const code = documentCodeResolver.resolve({
|
||||
avataxDocumentCode: payload.order?.avataxDocumentCode,
|
||||
orderId: payload.order?.id,
|
||||
});
|
||||
|
||||
return {
|
||||
code,
|
||||
lines,
|
||||
customerCode,
|
||||
addresses,
|
||||
date: new Date(),
|
||||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import {
|
|||
import { Logger, createLogger } from "../../lib/logger";
|
||||
|
||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||
import { getAppConfig } from "../app/get-app-config";
|
||||
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
||||
import { ProviderConnection } from "../provider-connections/provider-connections";
|
||||
|
@ -57,7 +58,11 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
|||
}
|
||||
|
||||
async cancelOrder(payload: OrderCancelledPayload) {
|
||||
this.client.cancelOrder(payload);
|
||||
return this.client.cancelOrder(payload);
|
||||
}
|
||||
|
||||
async refundOrder(payload: OrderRefundedPayload) {
|
||||
return this.client.refundOrder(payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
|||
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||
|
||||
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
||||
|
||||
|
@ -11,4 +12,5 @@ export interface ProviderWebhookService {
|
|||
calculateTaxes: (payload: CalculateTaxesPayload) => Promise<CalculateTaxesResponse>;
|
||||
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
||||
refundOrder: (payload: OrderRefundedPayload) => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ function joinAddresses(address1: string, address2: string): string {
|
|||
}
|
||||
|
||||
function mapSaleorAddressToTaxJarAddress(
|
||||
address: SaleorAddress
|
||||
address: SaleorAddress,
|
||||
): Pick<TaxParams, "to_city" | "to_country" | "to_state" | "to_street" | "to_zip"> {
|
||||
return {
|
||||
to_street: joinAddresses(address.streetAddress1, address.streetAddress2),
|
||||
|
@ -20,7 +20,7 @@ function mapSaleorAddressToTaxJarAddress(
|
|||
}
|
||||
|
||||
function mapChannelAddressToTaxJarAddress(
|
||||
address: TaxJarConfig["address"]
|
||||
address: TaxJarConfig["address"],
|
||||
): Pick<TaxParams, "from_city" | "from_country" | "from_state" | "from_street" | "from_zip"> {
|
||||
return {
|
||||
from_city: address.city,
|
||||
|
@ -31,7 +31,7 @@ function mapChannelAddressToTaxJarAddress(
|
|||
};
|
||||
}
|
||||
|
||||
function mapChannelAddressToAddressParams(address: TaxJarConfig["address"]): AddressParams {
|
||||
function mapChannelAddresstoParams(address: TaxJarConfig["address"]): AddressParams {
|
||||
return {
|
||||
city: address.city,
|
||||
country: address.country,
|
||||
|
@ -44,5 +44,5 @@ function mapChannelAddressToAddressParams(address: TaxJarConfig["address"]): Add
|
|||
export const taxJarAddressFactory = {
|
||||
fromSaleorToTax: mapSaleorAddressToTaxJarAddress,
|
||||
fromChannelToTax: mapChannelAddressToTaxJarAddress,
|
||||
fromChannelToParams: mapChannelAddressToAddressParams,
|
||||
fromChannelToParams: mapChannelAddresstoParams,
|
||||
};
|
||||
|
|
|
@ -9,19 +9,19 @@ export class TaxJarCalculateTaxesPayloadTransformer {
|
|||
constructor(private readonly config: TaxJarConfig) {}
|
||||
|
||||
transform(taxBase: TaxBaseFragment, matches: TaxJarTaxCodeMatches): TaxJarCalculateTaxesTarget {
|
||||
const fromAddress = taxJarAddressFactory.fromChannelToTax(this.config.address);
|
||||
const from = taxJarAddressFactory.fromChannelToTax(this.config.address);
|
||||
|
||||
if (!taxBase.address) {
|
||||
throw new Error("Customer address is required to calculate taxes in TaxJar.");
|
||||
}
|
||||
|
||||
const lineTransformer = new TaxJarCalculateTaxesPayloadLinesTransformer();
|
||||
const toAddress = taxJarAddressFactory.fromSaleorToTax(taxBase.address);
|
||||
const to = taxJarAddressFactory.fromSaleorToTax(taxBase.address);
|
||||
|
||||
const taxParams: TaxJarCalculateTaxesTarget = {
|
||||
params: {
|
||||
...fromAddress,
|
||||
...toAddress,
|
||||
...from,
|
||||
...to,
|
||||
shipping: taxBase.shippingPrice.amount,
|
||||
line_items: lineTransformer.transform(taxBase, matches),
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ const transformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
|
|||
|
||||
const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [
|
||||
{
|
||||
id: "T3JkZXJMaW5lOjE=",
|
||||
productSku: "sku",
|
||||
productName: "Test product",
|
||||
quantity: 1,
|
||||
|
@ -29,6 +30,7 @@ const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
id: "T3JkZXJMaW5lOjF=",
|
||||
productSku: "sku-2",
|
||||
productName: "Test product 2",
|
||||
quantity: 2,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { TaxJarOrderConfirmedTaxCodeMatcher } from "./taxjar-order-confirmed-tax
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const mockedLine: OrderLineFragment = {
|
||||
id: "T3JkZXJMaW5lOjE=",
|
||||
productSku: "sku",
|
||||
productName: "Test product",
|
||||
quantity: 1,
|
||||
|
|
|
@ -2,15 +2,15 @@ import { AuthData } from "@saleor/app-sdk/APL";
|
|||
import {
|
||||
OrderCancelledEventSubscriptionFragment,
|
||||
OrderConfirmedSubscriptionFragment,
|
||||
TaxBaseFragment,
|
||||
} from "../../../generated/graphql";
|
||||
import { Logger, createLogger } from "../../lib/logger";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
||||
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
||||
import { TaxJarClient } from "./taxjar-client";
|
||||
import { TaxJarConfig } from "./taxjar-connection-schema";
|
||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||
|
||||
export class TaxJarWebhookService implements ProviderWebhookService {
|
||||
client: TaxJarClient;
|
||||
|
@ -47,6 +47,12 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
|||
}
|
||||
|
||||
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
|
||||
// TaxJar isn't implemented yet
|
||||
// todo: implement
|
||||
this.logger.debug("cancelOrder not implement for TaxJar");
|
||||
}
|
||||
|
||||
async refundOrder(payload: OrderRefundedPayload) {
|
||||
// todo: implement
|
||||
this.logger.debug("refundOrder not implement for TaxJar");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { orderCalculateTaxesSyncWebhook } from "./webhooks/order-calculate-taxes
|
|||
import { orderConfirmedAsyncWebhook } from "./webhooks/order-confirmed";
|
||||
import { REQUIRED_SALEOR_VERSION } from "../../../saleor-app";
|
||||
import { orderCancelledAsyncWebhook } from "./webhooks/order-cancelled";
|
||||
import { orderRefundedAsyncWebhook } from "./webhooks/order-refunded";
|
||||
|
||||
export default createManifestHandler({
|
||||
async manifestFactory({ appBaseUrl }) {
|
||||
|
@ -37,6 +38,7 @@ export default createManifestHandler({
|
|||
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderCancelledAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
orderRefundedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||
],
|
||||
};
|
||||
|
||||
|
|
56
apps/taxes/src/pages/api/webhooks/order-refunded.ts
Normal file
56
apps/taxes/src/pages/api/webhooks/order-refunded.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
OrderRefundedEventSubscriptionFragment,
|
||||
UntypedOrderRefundedSubscriptionDocument,
|
||||
} 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,
|
||||
},
|
||||
};
|
||||
|
||||
export type OrderRefundedPayload = Extract<
|
||||
OrderRefundedEventSubscriptionFragment,
|
||||
{ __typename: "OrderRefunded" }
|
||||
>;
|
||||
|
||||
export const orderRefundedAsyncWebhook = new SaleorAsyncWebhook<OrderRefundedPayload>({
|
||||
name: "OrderRefunded",
|
||||
apl: saleorApp.apl,
|
||||
event: "ORDER_REFUNDED",
|
||||
query: UntypedOrderRefundedSubscriptionDocument,
|
||||
webhookPath: "/api/webhooks/order-refunded",
|
||||
});
|
||||
|
||||
export default orderRefundedAsyncWebhook.createHandler(async (req, res, ctx) => {
|
||||
const logger = createLogger({ event: ctx.event });
|
||||
const { payload } = ctx;
|
||||
const webhookResponse = new WebhookResponse(res);
|
||||
|
||||
logger.info("Handler called with payload");
|
||||
|
||||
if (!payload.order) {
|
||||
return webhookResponse.error(new Error("Insufficient order data"));
|
||||
}
|
||||
|
||||
try {
|
||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
||||
const channelSlug = payload.order.channel.slug;
|
||||
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
|
||||
|
||||
logger.info("Refunding order...");
|
||||
|
||||
await taxProvider.refundOrder(payload);
|
||||
|
||||
logger.info("Order refunded");
|
||||
|
||||
return webhookResponse.success();
|
||||
} catch (error) {
|
||||
return webhookResponse.error(error);
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue