Skip to content

Commit

Permalink
refactor order confirmed event (#1601)
Browse files Browse the repository at this point in the history
* SaleorOrderConfirmedEvent remove storing zod schema instead of payload

* Use event in transformed

* wip

* fix tests

* imprvoe types
  • Loading branch information
lkostrowski authored Oct 1, 2024
1 parent a41d2b0 commit 4fc1062
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 75 deletions.
5 changes: 2 additions & 3 deletions apps/avatax/src/modules/avatax/avatax-webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AvataxCalculateTaxesPayloadService } from "@/modules/avatax/calculate-t
import { AvataxCalculateTaxesPayloadTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer";
import { AvataxTaxCodeMatchesService } from "@/modules/avatax/tax-code/avatax-tax-code-matches.service";

import { DeprecatedOrderConfirmedSubscriptionFragment, SaleorOrderConfirmedEvent } from "../saleor";
import { SaleorOrderConfirmedEvent } from "../saleor";
import { CancelOrderPayload } from "../taxes/tax-provider-webhook";
import { CalculateTaxesPayload } from "../webhooks/payloads/calculate-taxes-payload";
import { AvataxConfig } from "./avatax-connection-schema";
Expand Down Expand Up @@ -43,14 +43,13 @@ export class AvataxWebhookService {
}

async confirmOrder(
order: DeprecatedOrderConfirmedSubscriptionFragment,
confirmedOrderEvent: SaleorOrderConfirmedEvent,
avataxConfig: AvataxConfig,
authData: AuthData,
discountStrategy: PriceReductionDiscountsStrategy,
) {
const response = await this.avataxOrderConfirmedAdapter.send(
{ order, confirmedOrderEvent },
{ confirmedOrderEvent },
avataxConfig,
authData,
discountStrategy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability
import { loggerContext } from "@/logger-context";

import { createLogger } from "../../../logger";
import {
DeprecatedOrderConfirmedSubscriptionFragment,
SaleorOrderConfirmedEvent,
} from "../../saleor";
import { SaleorOrderConfirmedEvent } from "../../saleor";
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient } from "../avatax-client";
Expand All @@ -19,12 +16,9 @@ import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-pay
import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";

type AvataxOrderConfirmedPayload = {
/**
* @deprecated use `SaleorOrderConfirmedEvent` instead
*/
order: DeprecatedOrderConfirmedSubscriptionFragment;
confirmedOrderEvent: SaleorOrderConfirmedEvent;
};

type AvataxOrderConfirmedResponse = CreateOrderResponse;

export class AvataxOrderConfirmedAdapter
Expand Down Expand Up @@ -53,7 +47,6 @@ export class AvataxOrderConfirmedAdapter
this.logger.debug("Transforming the Saleor payload for creating order with AvaTax...");

const target = await this.avataxOrderConfirmedPayloadService.getPayload(
payload.order,
payload.confirmedOrderEvent,
config,
authData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ const mockGenerator = new AvataxOrderConfirmedMockGenerator();
const saleorOrderConfirmedEventMock = SaleorOrderConfirmedEventMockFactory.create();
const discountsStrategy = new PriceReductionDiscountsStrategy();

const orderMock = mockGenerator.generateOrder();

/**
* TODO: Dont export this, extract to shared code
*/
Expand All @@ -36,7 +34,6 @@ const transformer = new AvataxOrderConfirmedPayloadTransformer(
describe("AvataxOrderConfirmedPayloadTransformer", () => {
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", async () => {
const payload = await transformer.transform({
order: orderMock,
confirmedOrderEvent: saleorOrderConfirmedEventMock,
avataxConfig: avataxConfigMock,
matches: [],
Expand All @@ -47,7 +44,6 @@ describe("AvataxOrderConfirmedPayloadTransformer", () => {
});
it("returns document type of SalesOrder when isDocumentRecordingEnabled is false", async () => {
const payload = await transformer.transform({
order: orderMock,
confirmedOrderEvent: saleorOrderConfirmedEventMock,
avataxConfig: {
...avataxConfigMock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { DocumentType } from "avatax/lib/enums/DocumentType";
import { err, ok } from "neverthrow";

import { createLogger } from "../../../logger";
import {
DeprecatedOrderConfirmedSubscriptionFragment,
SaleorOrderConfirmedEvent,
} from "../../saleor";
import { SaleorOrderConfirmedEvent } from "../../saleor";
import { TaxBadPayloadError } from "../../taxes/tax-error";
import { avataxAddressFactory } from "../address-factory";
import { AvataxCalculationDateResolver } from "../avatax-calculation-date-resolver";
Expand Down Expand Up @@ -37,50 +34,57 @@ export class AvataxOrderConfirmedPayloadTransformer {

return DocumentType.SalesInvoice;
}
private getSaleorAddress(order: DeprecatedOrderConfirmedSubscriptionFragment) {
if (order.shippingAddress) {
return ok(order.shippingAddress);
private getSaleorAddress(confirmedOrderEvent: SaleorOrderConfirmedEvent) {
const shippingAddress = confirmedOrderEvent.getOrderShippingAddress();
const billingAddress = confirmedOrderEvent.getOrderBillingAddress();

if (shippingAddress) {
return ok(shippingAddress);
}

if (order.billingAddress) {
if (billingAddress) {
this.logger.warn(
"OrderConfirmedPayload has no shipping address, falling back to billing address",
);

return ok(order.billingAddress);
return ok(billingAddress);
}

return err(new TaxBadPayloadError("OrderConfirmedPayload has no shipping or billing address"));
}
async transform({
order,
confirmedOrderEvent,
avataxConfig,
matches,
discountsStrategy,
}: {
order: DeprecatedOrderConfirmedSubscriptionFragment;
confirmedOrderEvent: SaleorOrderConfirmedEvent;
avataxConfig: AvataxConfig;
matches: AvataxTaxCodeMatches;
discountsStrategy: PriceReductionDiscountsStrategy;
}): Promise<CreateTransactionArgs> {
const entityUseCode = await this.avataxEntityTypeMatcher.match(order.avataxEntityCode);
const entityUseCode = await this.avataxEntityTypeMatcher.match(
confirmedOrderEvent.getAvaTaxEntityCode(),
);

const date = this.avataxCalculationDateResolver.resolve(
order.avataxTaxCalculationDate,
order.created,
confirmedOrderEvent.getAvaTaxTaxCalculationDate(),
confirmedOrderEvent.getOrderCreationDate(),
);

const code = this.avataxDocumentCodeResolver.resolve({
avataxDocumentCode: order.avataxDocumentCode,
orderId: order.id,
avataxDocumentCode: confirmedOrderEvent.getAvaTaxDocumentCode(),
orderId: confirmedOrderEvent.getOrderId(),
});

const customerCode = avataxCustomerCode.resolve({
avataxCustomerCode: order.avataxCustomerCode,
legacyAvataxCustomerCode: order.user?.avataxCustomerCode,
legacyUserId: order.user?.id,
avataxCustomerCode: confirmedOrderEvent.getAvaTaxCustomerCode(),
legacyAvataxCustomerCode: confirmedOrderEvent.getLegacyAvaTaxCustomerCode(),
legacyUserId: confirmedOrderEvent.getUserId(),
source: "Order",
});
const addressPayload = this.getSaleorAddress(order);

const addressPayload = this.getSaleorAddress(confirmedOrderEvent);

if (addressPayload.isErr()) {
Sentry.captureException(addressPayload.error);
Expand All @@ -105,9 +109,9 @@ export class AvataxOrderConfirmedPayloadTransformer {
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
shipTo: avataxAddressFactory.fromSaleorAddress(addressPayload.value),
},
currencyCode: order.total.currency,
currencyCode: confirmedOrderEvent.getOrderCurrency(),
// we can fall back to empty string because email is not a required field
email: order.user?.email ?? order.userEmail ?? "",
email: confirmedOrderEvent.resolveUserEmailOrEmpty(),
lines: this.saleorOrderToAvataxLinesTransformer.transform({
confirmedOrderEvent,
matches,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { AuthData } from "@saleor/app-sdk/APL";

import {
DeprecatedOrderConfirmedSubscriptionFragment,
SaleorOrderConfirmedEvent,
} from "../../saleor";
import { SaleorOrderConfirmedEvent } from "../../saleor";
import { CreateTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { PriceReductionDiscountsStrategy } from "../discounts";
Expand All @@ -22,7 +19,6 @@ export class AvataxOrderConfirmedPayloadService {
}

async getPayload(
order: DeprecatedOrderConfirmedSubscriptionFragment,
confirmedOrderEvent: SaleorOrderConfirmedEvent,
avataxConfig: AvataxConfig,
authData: AuthData,
Expand All @@ -31,7 +27,6 @@ export class AvataxOrderConfirmedPayloadService {
const matches = await this.getMatches(authData);

return this.avataxOrderConfirmedPayloadTransformer.transform({
order,
confirmedOrderEvent,
avataxConfig,
matches,
Expand Down
1 change: 0 additions & 1 deletion apps/avatax/src/modules/saleor/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./order-cancelled";
export * from "./order-confirmed";
export * from "./order-line";
export * from "./types";
40 changes: 40 additions & 0 deletions apps/avatax/src/modules/saleor/order-confirmed/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,44 @@ describe("SaleorOrderConfirmedEvent", () => {
expect(event.getShippingAmount()).toEqual(payload.order.shippingPrice.net.amount);
});
});

describe("resolveUserEmailOrEmpty", () => {
it("Returns order.user.email if exists", () => {
const payload = SaleorOrderConfirmedEventMockFactory.getGraphqlPayload();

payload.order.user = {
email: "a@b.com",
id: "1",
};

payload.order.userEmail = "another@another.com";

const result = SaleorOrderConfirmedEvent.createFromGraphQL(payload);

expect(result._unsafeUnwrap().resolveUserEmailOrEmpty()).toBe("a@b.com");
});

it("Returns order.userEmail.email if exists and user.email doesnt", () => {
const payload = SaleorOrderConfirmedEventMockFactory.getGraphqlPayload();

payload.order.user = undefined;

payload.order.userEmail = "another@another.com";

const result = SaleorOrderConfirmedEvent.createFromGraphQL(payload);

expect(result._unsafeUnwrap().resolveUserEmailOrEmpty()).toBe("another@another.com");
});

it("Returns empty string if neither user.email or userEmail exist", () => {
const payload = SaleorOrderConfirmedEventMockFactory.getGraphqlPayload();

payload.order.user = undefined;
payload.order.userEmail = undefined;

const result = SaleorOrderConfirmedEvent.createFromGraphQL(payload);

expect(result._unsafeUnwrap().resolveUserEmailOrEmpty()).toBe("");
});
});
});
64 changes: 50 additions & 14 deletions apps/avatax/src/modules/saleor/order-confirmed/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import { BaseError } from "../../../error";
import { OrderConfirmedPayload } from "../../webhooks/payloads/order-confirmed-payload";
import { SaleorOrderLine } from "../order-line";

type EventWithOrder = Omit<OrderConfirmedPayload, "order"> & {
order: NonNullable<OrderConfirmedPayload["order"]>;
};

export class SaleorOrderConfirmedEvent {
private static schema = z.object({
/**
* While GraphQL provides types contract, not everything can be consumed by the app.
* For example App requires lines or shipping to calculate taxes.
*
* Schema here is additional validation - if these fields don't exist, app must handle gracefully lack of data in payload
*/
private static requiredFieldsSchema = z.object({
order: z.object({
channel: z.object({
taxConfiguration: z.object({
Expand All @@ -29,8 +39,8 @@ export class SaleorOrderConfirmedEvent {
});

private constructor(
private data: z.infer<typeof SaleorOrderConfirmedEvent.schema>,
private lines: SaleorOrderLine[],
private rawPayload: EventWithOrder,
private parsedLines: SaleorOrderLine[],
) {}

static ParsingError = BaseError.subclass("SaleorOrderConfirmedEventParsingError");
Expand All @@ -45,7 +55,7 @@ export class SaleorOrderConfirmedEvent {
}

const parser = Result.fromThrowable(
SaleorOrderConfirmedEvent.schema.parse,
SaleorOrderConfirmedEvent.requiredFieldsSchema.parse,
SaleorOrderConfirmedEvent.ParsingError.normalize,
);

Expand All @@ -63,26 +73,52 @@ export class SaleorOrderConfirmedEvent {
return err(parsedLinePayload.error);
}

return ok(new SaleorOrderConfirmedEvent(parsedPayload.value, parsedLinePayload.value));
return ok(new SaleorOrderConfirmedEvent(payload as EventWithOrder, parsedLinePayload.value));
};

getChannelSlug = () => this.data.order.channel.slug;
getChannelSlug = () => this.rawPayload.order.channel.slug;

getOrderId = () => this.data.order.id;
getOrderId = () => this.rawPayload.order.id;

isFulfilled = () => this.data.order.status === "FULFILLED";
isFulfilled = () => this.rawPayload.order.status === "FULFILLED";

isStrategyFlatRates = () =>
this.data.order.channel.taxConfiguration.taxCalculationStrategy === "FLAT_RATES";
this.rawPayload.order.channel.taxConfiguration.taxCalculationStrategy === "FLAT_RATES";

getIsTaxIncluded = () => this.data.order.channel.taxConfiguration.pricesEnteredWithTax;
getIsTaxIncluded = () => this.rawPayload.order.channel.taxConfiguration.pricesEnteredWithTax;

getLines = () => this.lines;
getLines = () => this.parsedLines;

hasShipping = () => this.data.order.shippingPrice.net.amount !== 0;
hasShipping = () => this.rawPayload.order.shippingPrice.net.amount !== 0;

getShippingAmount = () =>
this.getIsTaxIncluded()
? this.data.order.shippingPrice.gross.amount
: this.data.order.shippingPrice.net.amount;
? this.rawPayload.order.shippingPrice.gross.amount
: this.rawPayload.order.shippingPrice.net.amount;

resolveUserEmailOrEmpty = () =>
this.rawPayload.order.user?.email ?? this.rawPayload.order?.userEmail ?? "";

getOrderCurrency = () => this.rawPayload.order.total.currency;

getUserId = () => this.rawPayload.order.user?.id;

/**
* @deprecated - We should use customer code from order
*/
getLegacyAvaTaxCustomerCode = () => this.rawPayload.order.user?.avataxCustomerCode;

getAvaTaxCustomerCode = () => this.rawPayload.order.avataxCustomerCode;

getAvaTaxDocumentCode = () => this.rawPayload.order.avataxDocumentCode;

getAvaTaxEntityCode = () => this.rawPayload.order.avataxEntityCode;

getAvaTaxTaxCalculationDate = () => this.rawPayload.order.avataxTaxCalculationDate;

getOrderCreationDate = () => this.rawPayload.order.created;

getOrderShippingAddress = () => this.rawPayload.order.shippingAddress;

getOrderBillingAddress = () => this.rawPayload.order.shippingAddress;
}
Loading

0 comments on commit 4fc1062

Please sign in to comment.