medusajs / medusa

Building blocks for digital commerce
https://medusajs.com
MIT License
24.45k stars 2.4k forks source link

TaxCalculationStrategy called many times in 'orders' page of admin dashboard #5772

Open JeffYYW opened 9 months ago

JeffYYW commented 9 months ago

Bug report

Describe the bug

I'm looking to override the tax calculation strategy as referenced in this guide to integrate a third-party service (Stripe Tax).

The issue is that the calculate method seems to get called 30+ times when I visit and refresh the 'orders' page in the admin at app/a/orders

System information

Medusa version (including plugins):

 "@medusajs/admin": "^7.1.1",
    "@medusajs/cache-inmemory": "^1.8.8",
    "@medusajs/cache-redis": "^1.8.8",
    "@medusajs/event-bus-local": "^1.9.6",
    "@medusajs/event-bus-redis": "^1.8.9",
    "@medusajs/file-local": "^1.0.2",
    "@medusajs/icons": "^1.0.0",
    "@medusajs/inventory": "^1.10.0",
    "@medusajs/medusa": "^1.17.3",
    "@medusajs/stock-location": "^1.10.0",
    "@medusajs/ui": "^1.0.0",
    "medusa-file-spaces": "^1.4.0",
    "medusa-fulfillment-manual": "^1.1.38",
    "medusa-interfaces": "^1.3.7",
    "medusa-payment-manual": "^1.0.24",
    "medusa-payment-stripe": "^6.0.5",
    "medusa-plugin-postmark": "^4.3.0",

Node.js version: 21.1.0 Database: PostGres Operating system: MacOS Browser (if relevant):

Steps to reproduce the behavior

  1. Create src/strategies/tax-calculation.ts
  2. Implement basic calculate method as in code snippet below
  3. Visit the admin dashboard at /app/a/orders
  4. Refresh the page and see multiple calls to the calculate method in server logs

Expected behavior

TaxCalculationStrategy override should not be called when viewing the admin dashboard.

Screenshots

Screenshot 2023-11-30 at 2 35 56 PM

Code snippets

src/strategies/tax-calculation.ts

import {
  ITaxCalculationStrategy,
  LineItem,
  LineItemTaxLine,
  ShippingMethodTaxLine,
  TaxCalculationContext,
} from "@medusajs/medusa";

class TaxCalculationStrategy implements ITaxCalculationStrategy {
  async calculate(
    items: LineItem[],
    taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
    calculationContext: TaxCalculationContext
  ): Promise<number> {
    console.log("tax strategy override");
    return 0;
  }
}

export default TaxCalculationStrategy;

Additional context

I'd be looking to do something like below and call the Stripe Tax calculation API

async calculate(
    items: LineItem[],
    taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
    calculationContext: TaxCalculationContext
  ): Promise<number> {
    const { shipping_address } = calculationContext;

    try {
      const stripeTaxCalculation =
        await this.stripeTaxCalculationService_.calculateTax({
          items,
          country_code: shipping_address.country_code,
          postal_code: shipping_address.postal_code,
          province: shipping_address.province,
        });

      return stripeTaxCalculation.tax_amount_exclusive;
    } catch (error) {
      console.error("Error during tax calculation:", error);
      return 0;
    }
  }
vholik commented 2 months ago

+1

vholik commented 2 months ago

@JeffYYW You can use Medusa cache module to not create a calculation every time, here is an example:

class TaxCalculationStrategy implements ITaxCalculationStrategy {
  protected readonly logger: Logger;
  protected readonly stripeService_: StripeService;
  protected readonly cacheService_: ICacheService;

  constructor({ logger, stripeService, cacheService }) {
    this.logger = logger;
    this.stripeService_ = stripeService;
    this.cacheService_ = cacheService;
  }

  private deepSortObject = (obj: Record<string, any>) => {
    if (Array.isArray(obj)) {
      return _.sortBy(obj.map(this.deepSortObject), JSON.stringify);
    } else if (_.isObject(obj)) {
      const sortedEntries = _.toPairs(obj).sort();
      const sortedObject = {};
      sortedEntries.forEach(([key, value]) => {
        sortedObject[key] = this.deepSortObject(value);
      });
      return sortedObject;
    }
    return obj;
  };

  private serializeCalculationContext(context: Record<string, any>) {
    const sortedContext = this.deepSortObject(context);
    return JSON.stringify(sortedContext);
  }

  private generateCartHash(input: string) {
    return crypto.createHash("sha256").update(input).digest("hex");
  }

  async calculate(
    items: LineItem[],
    taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
    calculationContext: TaxCalculationContext
  ): Promise<number> {
    const { shipping_address, region } = calculationContext;

    const context = {
      currency: region.currency_code,
      line_items: items,
      shipping_address: shipping_address,
    };

    // Get neccessary data from the calculation context
    const transformedContext =
      this.stripeService_.getTranformedCalculationContext(context);

    // Serialize the context to a string
    const serializedCart = this.serializeCalculationContext(transformedContext);

    // Generate a hash from the serialized context
    const cacheKey = this.generateCartHash(serializedCart);

    // Check if the hash exists in the cache
    const cached: { value: number } = await this.cacheService_.get(cacheKey);

    if (cached) {
      return cached.value;
    }

    const calculatedTax = await this.stripeService_.calculateTax(context);

    await this.cacheService_.set(cacheKey, { value: calculatedTax }, 60);

    return calculatedTax;
  }
}