{"record":{"id":"f816d675faee5bbf","repo":"medusajs/medusa","slug":"unexpected-state-f816d6","errorCode":"unexpected_state","errorMessage":"Cannot lock store credit accounts outside of a transaction","messagePattern":"Cannot lock store credit accounts outside of a transaction","errorType":"error_code","errorClass":"MedusaError","httpStatus":500,"severity":"critical","filePath":"packages/plugins/loyalty/src/modules/store-credit/service.ts","lineNumber":354,"sourceCode":"    accountIds: string[],\n    @MedusaContext() sharedContext: Context = {}\n  ) {\n    const uniqueAccountIds = Array.from(new Set(accountIds)).sort();\n\n    if (!uniqueAccountIds.length) {\n      return;\n    }\n\n    const manager = sharedContext.transactionManager as SqlEntityManager;\n    const transactionContext = manager.getTransactionContext();\n\n    if (!transactionContext) {\n      /*\n        Falling back to a non-transactional connection would release the lock\n        immediately and silently reintroduce the double-spend race, so we fail\n        loudly instead.\n      */\n      throw new MedusaError(\n        MedusaError.Types.UNEXPECTED_STATE,\n        \"Cannot lock store credit accounts outside of a transaction\"\n      );\n    }\n\n    await transactionContext(\"store_credit_account\")\n      .select(\"id\")\n      .whereIn(\"id\", uniqueAccountIds)\n      .forUpdate();\n  }\n\n  @InjectTransactionManager()\n  async debitAccounts_(\n    debitAccountsData: ModuleDebitAccount[],\n    @MedusaContext() sharedContext: Context = {}\n  ) {\n    const manager = sharedContext.transactionManager as SqlEntityManager;\n    const transactions: ModuleAccountTransaction[] = [];","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/plugins/loyalty/src/modules/store-credit/service.ts#L336-L372","documentation":"lockAccountsForUpdate_ throws UNEXPECTED_STATE when it is invoked without a transactional context. Locking store credit accounts via SELECT ... FOR UPDATE only prevents double-spends if the lock and the subsequent writes share one transaction; a non-transactional connection would release the lock immediately, so the code fails loudly instead of silently reintroducing the race.","triggerScenarios":"Calling debit/credit flows (which call lockAccountsForUpdate_) without wrapping them in a module transaction, e.g. calling debitAccounts_ directly with a sharedContext that has no transactionManager, or a custom step that bypasses the transactional workflow path.","commonSituations":"Calling module methods from a workflow step that skips the distributed transaction, passing a plain context object instead of one created by the module's transaction manager, or upgrading the plugin and using an internal method externally.","solutions":["Invoke the debit/credit operations through the public workflow or service method that opens a transaction internally","Pass a context containing transactionManager (e.g. via @InjectTransactionManager or within a createStep transaction)","If calling directly, wrap the call in service.withTransaction(...) so the context carries a transaction"],"exampleFix":"// before\nawait storeCreditService.debitAccounts_(entries, {})\n// after\nawait storeCreditService.withTransaction(async (tx) => {\n  await tx.debitAccounts_(entries)\n})","handlingStrategy":"try-catch","validationCode":"if (!context.transactionManager) {\n  throw new Error('Open a transaction before debiting store credit')\n}","typeGuard":null,"tryCatchPattern":"try {\n  await service.withTransaction((tx) => tx.debitAccounts_(entries))\n} catch (e) {\n  if (e instanceof MedusaError && e.message.includes('outside of a transaction')) {\n    // re-run inside a transaction\n  }\n}","preventionTips":["Always use withTransaction for debit/credit","Never call internal locking methods outside workflows"],"tags":["transaction","locking","race-condition","store-credit"],"backgroundTag":"operation-requires-transaction","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}