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 {
|
fragment OrderConfirmedSubscription on Order {
|
||||||
id
|
id
|
||||||
number
|
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,
|
"private": true,
|
||||||
"saleor": {
|
"saleor": {
|
||||||
"schemaVersion": "3.10"
|
"schemaVersion": "3.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = {
|
||||||
},
|
},
|
||||||
lines: [
|
lines: [
|
||||||
{
|
{
|
||||||
|
id: "T3JkZXJMaW5lOjE=",
|
||||||
productSku: "328223580",
|
productSku: "328223580",
|
||||||
productName: "Monospace Tee",
|
productName: "Monospace Tee",
|
||||||
quantity: 3,
|
quantity: 3,
|
||||||
|
@ -71,6 +72,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "T3JkZXJMaW5lOjI=",
|
||||||
productSku: "328223581",
|
productSku: "328223581",
|
||||||
productName: "Monospace Tee",
|
productName: "Monospace Tee",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
@ -89,6 +91,7 @@ export const defaultOrder: OrderConfirmedSubscriptionFragment = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "T3JkZXJMaW5lOjM=",
|
||||||
productSku: "118223581",
|
productSku: "118223581",
|
||||||
productName: "Paul's Balance 420",
|
productName: "Paul's Balance 420",
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
|
|
|
@ -55,6 +55,11 @@ export type VoidTransactionArgs = {
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RefundTransactionParams = Pick<
|
||||||
|
CreateTransactionModel,
|
||||||
|
"customerCode" | "lines" | "date" | "addresses" | "code" | "companyCode"
|
||||||
|
>;
|
||||||
|
|
||||||
export class AvataxClient {
|
export class AvataxClient {
|
||||||
private client: Avatax;
|
private client: Avatax;
|
||||||
|
|
||||||
|
@ -112,4 +117,17 @@ export class AvataxClient {
|
||||||
filter: `code eq ${useCode}`,
|
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,
|
orderId,
|
||||||
}: {
|
}: {
|
||||||
avataxDocumentCode: string | null | undefined;
|
avataxDocumentCode: string | null | undefined;
|
||||||
orderId: string;
|
orderId: string | undefined;
|
||||||
}): string {
|
}): string {
|
||||||
/*
|
/*
|
||||||
* The value for "code" can be provided in the metadata.
|
* The value for "code" can be provided in the metadata.
|
||||||
|
@ -14,9 +14,12 @@ export class AvataxDocumentCodeResolver {
|
||||||
|
|
||||||
const code = avataxDocumentCode ?? orderId;
|
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.
|
* 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);
|
return code.slice(0, 20);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
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 { Logger, createLogger } from "../../lib/logger";
|
||||||
|
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
|
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
import { AvataxClient } from "./avatax-client";
|
import { AvataxClient } from "./avatax-client";
|
||||||
import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
|
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 { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
import { AvataxOrderRefundedAdapter } from "./order-refunded/avatax-order-refunded-adapter";
|
||||||
|
|
||||||
export class AvataxWebhookService implements ProviderWebhookService {
|
export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
config = defaultAvataxConfig;
|
config = defaultAvataxConfig;
|
||||||
|
@ -49,4 +51,10 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
|
|
||||||
await adapter.send(payload);
|
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 { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
|
import { AvataxAddressResolver } from "../order-confirmed/avatax-address-resolver";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesPayloadTransformer {
|
export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
private matchDocumentType(config: AvataxConfig): DocumentType {
|
||||||
|
@ -47,6 +48,11 @@ export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
);
|
);
|
||||||
|
|
||||||
const customerCode = this.resolveCustomerCode(payload);
|
const customerCode = this.resolveCustomerCode(payload);
|
||||||
|
const addressResolver = new AvataxAddressResolver();
|
||||||
|
const addresses = addressResolver.resolve({
|
||||||
|
from: avataxConfig.address,
|
||||||
|
to: payload.taxBase.address!,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
|
@ -56,10 +62,7 @@ export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
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: 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,
|
commit: avataxConfig.isAutocommit,
|
||||||
addresses: {
|
addresses,
|
||||||
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
|
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(payload.taxBase.address!),
|
|
||||||
},
|
|
||||||
currencyCode: payload.taxBase.currency,
|
currencyCode: payload.taxBase.currency,
|
||||||
lines: payloadLinesTransformer.transform(payload.taxBase, avataxConfig, matches),
|
lines: payloadLinesTransformer.transform(payload.taxBase, avataxConfig, matches),
|
||||||
date: new Date(),
|
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 { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
|
||||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||||
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
|
||||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
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 { AvataxDocumentCodeResolver } from "../avatax-document-code-resolver";
|
||||||
import { AvataxCalculationDateResolver } from "../avatax-calculation-date-resolver";
|
import { AvataxCalculationDateResolver } from "../avatax-calculation-date-resolver";
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
|
import { AvataxAddressResolver } from "./avatax-address-resolver";
|
||||||
|
|
||||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
|
@ -41,6 +41,12 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const addressResolver = new AvataxAddressResolver();
|
||||||
|
const addresses = addressResolver.resolve({
|
||||||
|
from: avataxConfig.address,
|
||||||
|
to: order.shippingAddress!,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
code,
|
code,
|
||||||
|
@ -50,11 +56,7 @@ export class AvataxOrderConfirmedPayloadTransformer {
|
||||||
companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode,
|
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: 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,
|
commit: avataxConfig.isAutocommit,
|
||||||
addresses: {
|
addresses,
|
||||||
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
|
|
||||||
// billing or shipping address?
|
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
|
||||||
},
|
|
||||||
currencyCode: order.total.currency,
|
currencyCode: order.total.currency,
|
||||||
email: taxProviderUtils.resolveStringOrThrow(order.user?.email),
|
email: taxProviderUtils.resolveStringOrThrow(order.user?.email),
|
||||||
lines: linesTransformer.transform(order, avataxConfig, matches),
|
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";
|
import { AvataxOrderConfirmedTaxCodeMatcher } from "./avatax-order-confirmed-tax-code-matcher";
|
||||||
|
|
||||||
const mockedLine: OrderLineFragment = {
|
const mockedLine: OrderLineFragment = {
|
||||||
|
id: "T3JkZXJMaW5lOjE=",
|
||||||
productSku: "sku",
|
productSku: "sku",
|
||||||
productName: "Test product",
|
productName: "Test product",
|
||||||
quantity: 1,
|
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 { Logger, createLogger } from "../../lib/logger";
|
||||||
|
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
|
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||||
import { getAppConfig } from "../app/get-app-config";
|
import { getAppConfig } from "../app/get-app-config";
|
||||||
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
||||||
import { ProviderConnection } from "../provider-connections/provider-connections";
|
import { ProviderConnection } from "../provider-connections/provider-connections";
|
||||||
|
@ -57,7 +58,11 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledPayload) {
|
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 { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
|
import { OrderRefundedPayload } from "../../pages/api/webhooks/order-refunded";
|
||||||
|
|
||||||
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
||||||
|
|
||||||
|
@ -11,4 +12,5 @@ export interface ProviderWebhookService {
|
||||||
calculateTaxes: (payload: CalculateTaxesPayload) => Promise<CalculateTaxesResponse>;
|
calculateTaxes: (payload: CalculateTaxesPayload) => Promise<CalculateTaxesResponse>;
|
||||||
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||||
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
|
||||||
|
refundOrder: (payload: OrderRefundedPayload) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ function joinAddresses(address1: string, address2: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSaleorAddressToTaxJarAddress(
|
function mapSaleorAddressToTaxJarAddress(
|
||||||
address: SaleorAddress
|
address: SaleorAddress,
|
||||||
): Pick<TaxParams, "to_city" | "to_country" | "to_state" | "to_street" | "to_zip"> {
|
): Pick<TaxParams, "to_city" | "to_country" | "to_state" | "to_street" | "to_zip"> {
|
||||||
return {
|
return {
|
||||||
to_street: joinAddresses(address.streetAddress1, address.streetAddress2),
|
to_street: joinAddresses(address.streetAddress1, address.streetAddress2),
|
||||||
|
@ -20,7 +20,7 @@ function mapSaleorAddressToTaxJarAddress(
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChannelAddressToTaxJarAddress(
|
function mapChannelAddressToTaxJarAddress(
|
||||||
address: TaxJarConfig["address"]
|
address: TaxJarConfig["address"],
|
||||||
): Pick<TaxParams, "from_city" | "from_country" | "from_state" | "from_street" | "from_zip"> {
|
): Pick<TaxParams, "from_city" | "from_country" | "from_state" | "from_street" | "from_zip"> {
|
||||||
return {
|
return {
|
||||||
from_city: address.city,
|
from_city: address.city,
|
||||||
|
@ -31,7 +31,7 @@ function mapChannelAddressToTaxJarAddress(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChannelAddressToAddressParams(address: TaxJarConfig["address"]): AddressParams {
|
function mapChannelAddresstoParams(address: TaxJarConfig["address"]): AddressParams {
|
||||||
return {
|
return {
|
||||||
city: address.city,
|
city: address.city,
|
||||||
country: address.country,
|
country: address.country,
|
||||||
|
@ -44,5 +44,5 @@ function mapChannelAddressToAddressParams(address: TaxJarConfig["address"]): Add
|
||||||
export const taxJarAddressFactory = {
|
export const taxJarAddressFactory = {
|
||||||
fromSaleorToTax: mapSaleorAddressToTaxJarAddress,
|
fromSaleorToTax: mapSaleorAddressToTaxJarAddress,
|
||||||
fromChannelToTax: mapChannelAddressToTaxJarAddress,
|
fromChannelToTax: mapChannelAddressToTaxJarAddress,
|
||||||
fromChannelToParams: mapChannelAddressToAddressParams,
|
fromChannelToParams: mapChannelAddresstoParams,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,19 +9,19 @@ export class TaxJarCalculateTaxesPayloadTransformer {
|
||||||
constructor(private readonly config: TaxJarConfig) {}
|
constructor(private readonly config: TaxJarConfig) {}
|
||||||
|
|
||||||
transform(taxBase: TaxBaseFragment, matches: TaxJarTaxCodeMatches): TaxJarCalculateTaxesTarget {
|
transform(taxBase: TaxBaseFragment, matches: TaxJarTaxCodeMatches): TaxJarCalculateTaxesTarget {
|
||||||
const fromAddress = taxJarAddressFactory.fromChannelToTax(this.config.address);
|
const from = taxJarAddressFactory.fromChannelToTax(this.config.address);
|
||||||
|
|
||||||
if (!taxBase.address) {
|
if (!taxBase.address) {
|
||||||
throw new Error("Customer address is required to calculate taxes in TaxJar.");
|
throw new Error("Customer address is required to calculate taxes in TaxJar.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineTransformer = new TaxJarCalculateTaxesPayloadLinesTransformer();
|
const lineTransformer = new TaxJarCalculateTaxesPayloadLinesTransformer();
|
||||||
const toAddress = taxJarAddressFactory.fromSaleorToTax(taxBase.address);
|
const to = taxJarAddressFactory.fromSaleorToTax(taxBase.address);
|
||||||
|
|
||||||
const taxParams: TaxJarCalculateTaxesTarget = {
|
const taxParams: TaxJarCalculateTaxesTarget = {
|
||||||
params: {
|
params: {
|
||||||
...fromAddress,
|
...from,
|
||||||
...toAddress,
|
...to,
|
||||||
shipping: taxBase.shippingPrice.amount,
|
shipping: taxBase.shippingPrice.amount,
|
||||||
line_items: lineTransformer.transform(taxBase, matches),
|
line_items: lineTransformer.transform(taxBase, matches),
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,7 @@ const transformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
|
||||||
|
|
||||||
const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [
|
const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [
|
||||||
{
|
{
|
||||||
|
id: "T3JkZXJMaW5lOjE=",
|
||||||
productSku: "sku",
|
productSku: "sku",
|
||||||
productName: "Test product",
|
productName: "Test product",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
@ -29,6 +30,7 @@ const mockedLines: OrderConfirmedSubscriptionFragment["lines"] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "T3JkZXJMaW5lOjF=",
|
||||||
productSku: "sku-2",
|
productSku: "sku-2",
|
||||||
productName: "Test product 2",
|
productName: "Test product 2",
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { TaxJarOrderConfirmedTaxCodeMatcher } from "./taxjar-order-confirmed-tax
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const mockedLine: OrderLineFragment = {
|
const mockedLine: OrderLineFragment = {
|
||||||
|
id: "T3JkZXJMaW5lOjE=",
|
||||||
productSku: "sku",
|
productSku: "sku",
|
||||||
productName: "Test product",
|
productName: "Test product",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
|
|
@ -2,15 +2,15 @@ import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import {
|
||||||
OrderCancelledEventSubscriptionFragment,
|
OrderCancelledEventSubscriptionFragment,
|
||||||
OrderConfirmedSubscriptionFragment,
|
OrderConfirmedSubscriptionFragment,
|
||||||
TaxBaseFragment,
|
|
||||||
} from "../../../generated/graphql";
|
} from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
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 { 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 { TaxJarClient } from "./taxjar-client";
|
import { TaxJarClient } from "./taxjar-client";
|
||||||
import { TaxJarConfig } from "./taxjar-connection-schema";
|
import { TaxJarConfig } from "./taxjar-connection-schema";
|
||||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
|
||||||
|
|
||||||
export class TaxJarWebhookService implements ProviderWebhookService {
|
export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
client: TaxJarClient;
|
client: TaxJarClient;
|
||||||
|
@ -47,6 +47,12 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledEventSubscriptionFragment) {
|
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 { 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 { orderRefundedAsyncWebhook } from "./webhooks/order-refunded";
|
||||||
|
|
||||||
export default createManifestHandler({
|
export default createManifestHandler({
|
||||||
async manifestFactory({ appBaseUrl }) {
|
async manifestFactory({ appBaseUrl }) {
|
||||||
|
@ -37,6 +38,7 @@ export default createManifestHandler({
|
||||||
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
checkoutCalculateTaxesSyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
orderConfirmedAsyncWebhook.getWebhookManifest(apiBaseURL),
|
||||||
orderCancelledAsyncWebhook.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