Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Unable to add additional injected dependencies to custom fulfillment provider #10487

Open
turacma opened this issue Dec 6, 2024 · 15 comments

Comments

@turacma
Copy link

turacma commented Dec 6, 2024

Package.json file

{
  "name": "ship",
  "version": "0.0.1",
  "description": "SHIP for Eight Sleep",
  "author": "Eight Sleep",
  "keywords": [
    "sqlite",
    "postgres",
    "typescript",
    "ecommerce",
    "headless",
    "medusa"
  ],
  "scripts": {
    "build": "medusa build",
    "start": "medusa start",
    "dev": "medusa develop",
    "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
    "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
  },
  "dependencies": {
    "@eight/logging": "^2.2.0",
    "@eight/practices": "^7.16.16",
    "@medusajs/admin-sdk": "2.1.0",
    "@medusajs/api-key": "2.1.0",
    "@medusajs/auth": "2.1.0",
    "@medusajs/auth-emailpass": "2.1.0",
    "@medusajs/cache-inmemory": "2.1.0",
    "@medusajs/cart": "2.1.0",
    "@medusajs/cli": "2.1.0",
    "@medusajs/currency": "2.1.0",
    "@medusajs/customer": "2.1.0",
    "@medusajs/event-bus-local": "2.1.0",
    "@medusajs/event-bus-redis": "2.1.0",
    "@medusajs/file": "2.1.0",
    "@medusajs/file-local": "2.1.0",
    "@medusajs/framework": "2.1.0",
    "@medusajs/fulfillment": "2.1.0",
    "@medusajs/fulfillment-manual": "2.1.0",
    "@medusajs/inventory": "2.1.0",
    "@medusajs/js-sdk": "^2.1.0",
    "@medusajs/medusa": "2.1.0",
    "@medusajs/notification": "2.1.0",
    "@medusajs/notification-local": "2.1.0",
    "@medusajs/order": "2.1.0",
    "@medusajs/payment": "2.1.0",
    "@medusajs/pricing": "2.1.0",
    "@medusajs/product": "2.1.0",
    "@medusajs/promotion": "2.1.0",
    "@medusajs/region": "2.1.0",
    "@medusajs/sales-channel": "2.1.0",
    "@medusajs/stock-location": "2.1.0",
    "@medusajs/store": "2.1.0",
    "@medusajs/tax": "2.1.0",
    "@medusajs/types": "2.1.0",
    "@medusajs/user": "2.1.0",
    "@medusajs/workflow-engine-inmemory": "2.1.0",
    "express": "^4.21.2",
    "stripe": "^17.4.0",
    "types-joi": "^2.1.0"
  },
  "devDependencies": {
    "@medusajs/test-utils": "2.1.0",
    "@mikro-orm/cli": "5.9.7",
    "@mikro-orm/core": "5.9.7",
    "@mikro-orm/migrations": "5.9.7",
    "@mikro-orm/postgresql": "5.9.7",
    "@stdlib/number-float64-base-normalize": "0.2.3",
    "@swc/core": "1.10.0",
    "@swc/jest": "^0.2.37",
    "@types/express": "^5.0.0",
    "@types/jest": "^29.5.14",
    "@types/node": "^22.10.1",
    "@types/react": "^18.3.12",
    "jest": "^29.7.0",
    "prop-types": "^15.8.1",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.2"
  },
  "engines": {
    "node": ">=20"
  }
}

Node.js version

v20.15.0

Database and its version

Aurora Postfres

Operating system name and version

Mac OS X 24.1.0

Browser name

N/A

What happended?

As part of my custom fulfillment provider's service I have a loader that creates an API client and registers it with the container.

    const trawlerApi = new TrawlerApi({
        ...
    });

    container.register("trawlerApi", asValue(trawlerApi));

But attempts to use that the api as an injected dependency fail. If I use the following format:

constructor(
       { logger, trawler } : InjectedDependencies,
        options: Options
    ) {
        super();

        this.logger_ = logger;
        this.trawlerApi_ = trawlerApi;
        this.options_ = options;
    }

I just get an error that my provider can't be loaded, if I use the following format:

    constructor(
        dependencies : InjectedDependencies,
        options: Options
    ) {
        super();

        this.logger_ = dependencies.logger;
        this.trawlerApi_ = dependencies.trawlerApi;
        this.options_ = options;
    }

I get the real error:

Uncaught AwilixResolutionError AwilixResolutionError: Could not resolve 'trawlerApi'.

Resolution path: trawlerApi
    at resolve (/Users/turacma/Workspace/ship/node_modules/awilix/lib/container.js:252:23)
    at get (/Users/turacma/Workspace/ship/node_modules/awilix/lib/container.js:66:33)
    at eval (repl:1:14)

I thought maybe it was a compatibility issue with the client, but I tried simply regisering a string value, and i get the same result.

Expected behavior

Dependencies registered in the loader would be available for use in the constructor of the service.

Actual behavior

Provider fails to load.

Link to reproduction repo

See minimal code above

@turacma turacma changed the title [Bug]: Unable to add additional injected dependencies to customer fulfillment provider [Bug]: Unable to add additional injected dependencies to custom fulfillment provider Dec 6, 2024
@srindom
Copy link
Collaborator

srindom commented Dec 6, 2024 via email

@turacma
Copy link
Author

turacma commented Dec 6, 2024

Yep, this is the work around I'm using. Was just trying to use a loader as it was recommended in the documentation for persistent connections. FWIW, the loader is executing, but nothing I register in the loader is available for consumption.

@turacma
Copy link
Author

turacma commented Dec 6, 2024

Can't seem to assess the they query engine either, which I was trying to access to work around the fact that the order object passed to the provider doesn't include the customer id or email.

@erickirt
Copy link
Contributor

erickirt commented Dec 7, 2024

Same here, for me it is a custom payment provider

@erickirt
Copy link
Contributor

erickirt commented Dec 7, 2024

I will share my code too

@erickirt
Copy link
Contributor

erickirt commented Dec 7, 2024

if you take out the query, it will work with absolutely no problem. I even had to take it out of the workflow.

If you add back query(like below) you get error that says the entire payment provider is not found. But take it out and it works. Also wanted to point out i tried container registration keys, without the keys, resolving from string, both of the helper steps in workflows(useRemoteQuery & useQueryGraph). They did not work, which is why i am kind of confused on logger is working since it is also a core feature.

import { AbstractPaymentProvider } from "@medusajs/framework/utils";
import {
  CreatePaymentProviderSession,
  PaymentProviderError,
  PaymentProviderSessionResponse,
  PaymentSessionStatus,
  Logger,
  Query,
  MedusaContainer,
  PaymentProviderContext,
} from "@medusajs/framework/types";
import { handleTransactionWorkflow } from "../../workflows/wallet/handle-transaction/handle-transaction-workflow";
import { TransactionType } from "../../modules/wallet/models/transaction";
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";

type InjectedDependencies = {
  logger: Logger;
  query: Query;
};

class TestPaymentProviderService extends AbstractPaymentProvider {
  static identifier = "test_payment";
  protected logger_: Logger;
  protected query_: Query;

  constructor({ logger, query }: InjectedDependencies) {
    super(arguments[0]);
    this.logger_ = logger;
    this.query_ = query; // Assign the injected query service to a class property

    this.logger_.info(
      "TestPaymentProviderService initialized with query dependency.",
    );
  }

  async initiatePayment(
    input: CreatePaymentProviderSession,
  ): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
    const { amount, currency_code, context } = input;

    this.logger_.info(
      JSON.stringify({
        message: "Initiating payment session",
        data: {
          amount,
          currency_code,
          context,
        },
      }),
    );

    const okie = this.query_.graph({
      entity: "wallet",
      fields: ["id", "balance"],
    });

    this.logger_.info(
      JSON.stringify({
        message: "Just did a query lets see",
        data: {
          okie,
        },
      }),
    );
    // Store customer_id in the session data since it's available in context here
    return {
      data: {
        amount,
        currency_code,
        customer_id: context.customer?.id,
      },
    };
  }

  async authorizePayment(
    paymentSessionData: Record<string, unknown>,
    context: PaymentProviderContext,
  ): Promise<{
    status: PaymentSessionStatus;
    data: Record<string, unknown>;
  }> {
    try {
      this.logger_.info(
        JSON.stringify({
          message: "Authorizing payment",
          step: "start",
          data: paymentSessionData,
          context,
        }),
      );

      const amount = Number(paymentSessionData.amount);
      if (isNaN(amount)) {
        throw new Error("Invalid amount in payment session data");
      }

      const customerId = paymentSessionData.customer_id as string;
      if (!customerId) {
        throw new Error("Customer ID is required in session data");
      }

      const container = this.container as unknown as MedusaContainer;

      const result = await handleTransactionWorkflow(container).run({
        input: {
          customer_id: customerId,
          amount,
          type: TransactionType.DEBIT,
          description: "Test payment authorization",
          metadata: {
            payment_method: "test_payment",
          },
        },
      });

      this.logger_.info(
        JSON.stringify({
          message: "Authorizing payment",
          step: "after_workflow",
          data: { result },
        }),
      );

      if (!result) {
        return {
          status: "error",
          data: {
            message: "Wallet not found",
            code: "not_found",
          },
        };
      }

      return {
        status: "authorized",
        data: paymentSessionData,
      };
    } catch (e: any) {
      this.logger_.error(
        JSON.stringify({
          message: "Error in authorizePayment",
          error: e.message,
          stack: e.stack,
          payment_session_data: paymentSessionData,
        }),
      );
      return {
        status: "error",
        data: {
          message: e.message || "Failed to authorize payment",
          code: "authorization_failed",
        },
      };
    }
  }

  async capturePayment(
    paymentData: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    try {
      const amount = Number(paymentData.amount);
      if (isNaN(amount)) {
        throw new Error("Invalid amount in payment data");
      }

      const customerId = paymentData.customer_id as string;
      if (!customerId) {
        throw new Error("Customer ID is required");
      }

      const container = this.container as unknown as MedusaContainer;

      const result = await handleTransactionWorkflow(container).run({
        input: {
          customer_id: customerId,
          amount,
          type: TransactionType.DEBIT,
          description: "Test payment capture",
          metadata: {
            payment_method: "test_payment",
          },
        },
      });

      this.logger_.info(JSON.stringify({ message: "Workflow result", result }));

      return {
        ...paymentData,
      };
    } catch (e: any) {
      this.logger_.error(
        JSON.stringify({
          message: "Error in capturePayment",
          error: e.message,
          stack: e.stack,
        }),
      );
      return {
        error: e.message || "Failed to capture payment",
        code: "capture_failed",
        detail: e.message,
      };
    }
  }

  async refundPayment(
    paymentData: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    try {
      const amount = Number(paymentData.amount);
      if (isNaN(amount)) {
        throw new Error("Invalid amount in payment data");
      }

      const customerId = paymentData.customer_id as string;
      if (!customerId) {
        throw new Error("Customer ID is required");
      }

      const container = this.container as unknown as MedusaContainer;

      const result = await handleTransactionWorkflow(container).run({
        input: {
          customer_id: customerId,
          amount,
          type: TransactionType.CREDIT,
          description: "Test payment refund",
          metadata: {
            payment_method: "test_payment",
          },
        },
      });

      this.logger_.info(JSON.stringify({ message: "Workflow result", result }));

      return {
        ...paymentData,
      };
    } catch (e: any) {
      this.logger_.error(
        JSON.stringify({
          message: "Error in refundPayment",
          error: e.message,
          stack: e.stack,
        }),
      );
      return {
        error: e.message || "Failed to refund payment",
        code: "refund_failed",
        detail: e.message,
      };
    }
  }

  async cancelPayment(
    paymentData: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    return {};
  }

  async deletePayment(
    paymentData: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    return {};
  }

  async retrievePayment(
    paymentData: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    return {};
  }

  async updatePayment(
    sessionData: Record<string, unknown>,
  ): Promise<PaymentProviderSessionResponse> {
    return { data: {} };
  }

  async getPaymentStatus(
    paymentData: Record<string, unknown>,
  ): Promise<PaymentSessionStatus> {
    return "authorized";
  }

  async getWebhookActionAndData() {
    return null;
  }
}

export default TestPaymentProviderService;

@turacma
Copy link
Author

turacma commented Dec 9, 2024

@srindom any suggestions on work arounds? Not having access to the rest of the customer information is blocker for some of our integrations.

@olivermrbl
Copy link
Contributor

olivermrbl commented Dec 9, 2024

@turacma, you can specify a module service's injected dependencies in the module config. Can you try to specify trawlerApi like so:

import { loadEnv, defineConfig } from '@medusajs/framework/utils'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    http: { ... },
  },
  modules: [
    {
      resolve: "@medusajs/fulfillment",
      dependencies: ["trawlerApi"], // <----------- Try to specify here it
      options: {
        providers: [{ ... }]
      }
    }
  ]
})

@olivermrbl
Copy link
Contributor

If you have a small reproduction in repo, I am happy to take a look at it

@turacma
Copy link
Author

turacma commented Dec 9, 2024

@olivermrbl that seems to work for query, but not for my custom dependency. It no longer blows up, but the dependency resolves as undefined. This at least unblocks me for now.

@erickirt
Copy link
Contributor

erickirt commented Dec 9, 2024

@turacma How did you get it working for query?

This didn't work for me:
dependencies: ["query"],

@erickirt
Copy link
Contributor

My code:

{
resolve: "@medusajs/medusa/payment", // DO HYPERSWITCH PAYMENT
dependencies: ["query"],
options: {
providers: [
{
resolve: "./src/modules/test-payment-provider",
id: "test_payment",
},
],
},
},

@erickirt
Copy link
Contributor

neither did this:
{
resolve: "@medusajs/medusa/payment", // DO HYPERSWITCH PAYMENT
dependencies: [ContainerRegistrationKeys.QUERY], // Use the container registration key
options: {
providers: [
{
resolve: "./src/modules/test-payment-provider",
id: "test_payment",
},
],
},
},

@erickirt
Copy link
Contributor

error: {"message":"Error in authorizePayment","error":"Could not resolve 'query'.\n\nResolution path: query","stack":"AwilixResolutionError: Could not resolve 'query'.\n\nResolution path: query\n

@turacma
Copy link
Author

turacma commented Dec 10, 2024

@erickirt the payment module may behave differently. I simply added query to the dependencies of the fulfillment module.

    {
      resolve: "@medusajs/medusa/fulfillment",
      dependencies: ["trawlerApi", "query"],
      options: {
        providers: [
          {
            resolve: "./src/modules/trawler",
            id: "trawler",
            options: {},
          },
        ],
      },
    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants