Files
clinicpro/docs/api/payment-method.md
T
hamedandClaude Opus 5 c9d4348c46 feat(tenant): mark the financial tables with their owning environment
Phase 6 of the tenant series. GlobalTables::DEFERRED is now empty and the
coverage test asserts it stays that way.

payments carries the (entity_type, entity_id) pair and belongs to the
receiving side, never the payer: an appointment payment takes the
appointment's environment, a subscription takes the environment its buyer
owns, and an SMS wallet top-up takes the wallet's. The patient never chose
an environment, so TenantFilter stays off for them and they still see their
own payment.

Three corrections to the analysis the phase was planned on, each backed by
the code or the data rather than the plan:

- A third payment type exists. Payment::TYPE_SMS_WALLET is created in
  SmsWalletController and already carries its environment in the metadata;
  without assigning it the write would fail at flush.
- clinic_subscriptions has no user_id, and its trial rows carry no payment,
  so it cannot drive the subscription backfill. The environment is derived
  the way handleSubscriptionActivation derives it — and that method now
  reads the pair off the payment instead of re-deriving it, so a payment and
  the subscription it buys can no longer land on different environments.
- WalletTransaction is not a child of Payment. payment_id is nullable and
  none of the four creation sites set it; the wallet is a person's, with a
  running balance per user. It and Settlement, which withdraws from that same
  wallet, are global with a recorded reason instead.

bank_accounts and pos_devices move from the registering user to the
environment. Their pair is deliberately nullable: nothing in the existing
data says which of a multi-environment owner's cards belongs where, and
guessing would point real money at the wrong account. Ambiguous rows stay
unassigned and the migration reports how many. The cost is that such a row
is invisible in every environment, so the owner reaches it through a
user-scoped lookup that runs outside the filter, and assigns it with
PATCH .../{uuid}/environment. The admin panel marks those rows and offers
the assignment.

Tests: 896 backend (+11), 570 frontend (+4). PHPStan unchanged at its 17
pre-existing errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:06:28 +03:30

7.9 KiB

Payment Methods API

Prefix: /api/v1/my/payment-methods

Per-environment payment methods managed from the settings screen (/admin/my-financial, tab "مدیریت پرداخت"). Two resources: bank accounts and POS (card reader) devices.

دسترسی منشی: روش‌های پرداخت زیرمجموعهٔ منبع payments هستند. برای ROLE_SECRETARY (SecretaryAccessChecker): GET→payments.view, POST→payments.create, PUT/PATCH→payments.update؛ نبودِ مجوز یا رابطهٔ فعال → 403. جزئیات: secretary.md. Records are stored so a patient invoice can later reference which account/device a service payment was made to.

All endpoints are scoped to the active environment (EntityContextResolver), not to the acting user. A doctor who also owns a clinic sees a different set of cards in each environment; switching context switches the list. user_id is still stored, but only records who registered the card.

کارت‌های بدون محیط. ردیف‌هایی که پیش از این نشانه‌گذاری ثبت شده‌اند و مالکشان بیش از یک محیط دارد، entity_type: null می‌گیرند: هیچ ستونی نمی‌گفت کارت مال کدام محیط است و حدس زدنش یعنی پول به حساب اشتباه.

چنین ردیفی در هیچ محیطی «متعلق» نیست (TenantFilter شرط تساوی می‌گذارد و NULL با هیچ مقداری برابر نیست) و تا تعیین محیط قابل ویرایش نیست — ولی در فهرست خودِ مالک می‌آید تا با PATCH .../{uuid}/environment به محیط فعال منتسبش کند. این کوئری عمداً بیرون فیلتر و محدود به user_id اجرا می‌شود. جزئیات: architecture/tenancy.md.

اگر محیط فعالی حل نشود (مثلاً نقش کلینیک بدون کلینیکِ واقعی، یا منشیِ بدون UserActiveContext) همهٔ این endpointها 403 ERR_FORBIDDEN_001 می‌دهند.

Permission: authenticated user with one of ROLE_CLINIC, ROLE_DOCTOR, ROLE_SECRETARY, ROLE_ADMIN (otherwise 403 ERR_FORBIDDEN_001).


Bank accounts

GET /api/v1/my/payment-methods/bank-accounts

List the bank accounts of the active environment (newest first), followed by any of the caller's own cards that still have no environment.

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "b73d0c8e-3833-4314-83ba-937b6d4dbc60",
      "bank_name": "ملی",
      "card_number": "6037991234567890",
      "account_number": "0101234567890",
      "shaba_number": "IR820540102680020817909002",
      "is_active": true,
      "created_at": 1785238315,
      "entity_type": "clinic"
    },
    {
      "uuid": "21225430-0108-4adf-99f1-978e0a864c71",
      "bank_name": "ملت",
      "card_number": null,
      "account_number": "0209876543210",
      "shaba_number": null,
      "is_active": true,
      "created_at": 1785238315,
      "entity_type": null
    }
  ]
}
Field Type Description
entity_type "doctor" | "clinic" | null محیطِ مالک؛ null یعنی هنوز تعیین نشده و کارت قابل استفاده نیست

Empty list returns "data": [].


POST /api/v1/my/payment-methods/bank-accounts

Create a bank account.

Body

Field Type Required Description
bank_name string Bank name
account_number string Account number
card_number string Card number
shaba_number string IBAN / SHABA

Response 201

Single created record (same shape as list item). entity_type is the active environment.

Errors

  • 422 ERR_VALIDATION_001bank_name or account_number missing (field set).

PUT /api/v1/my/payment-methods/bank-accounts/{uuid}

Update a bank account. Any subset of the create fields may be sent; only provided keys change. Empty bank_name/account_number422.

Response 200

Updated record.

Errors

  • 404 ERR_NOT_FOUND_001 — uuid unknown, owned by another environment, or still unassigned.
  • 422 ERR_VALIDATION_001 — provided bank_name/account_number empty.

PATCH /api/v1/my/payment-methods/bank-accounts/{uuid}/status

Toggle is_active (active ⇄ inactive). No body.

Response 200

Record with flipped is_active.

Errors

  • 404 ERR_NOT_FOUND_001 — uuid unknown, owned by another environment, or still unassigned.

PATCH /api/v1/my/payment-methods/bank-accounts/{uuid}/environment

کارتِ بی‌محیطِ خودِ کاربر را به محیط فعال می‌چسباند. بدون بدنه.

شرط‌های مالکیت و بی‌محیط بودن داخل خودِ UPDATE هستند، پس دو درخواست هم‌زمان نمی‌توانند یک کارت را به دو محیط بچسبانند و کارتِ محیط‌دار هم ربوده نمی‌شود.

Response 200

{
  "success": true,
  "data": {
    "uuid": "21225430-0108-4adf-99f1-978e0a864c71",
    "bank_name": "ملت",
    "card_number": null,
    "account_number": "0209876543210",
    "shaba_number": null,
    "is_active": true,
    "created_at": 1785238315,
    "entity_type": "clinic"
  }
}

Errors

404 ERR_NOT_FOUND_001 — uuid ناشناس، مالِ کاربر دیگر، یا از قبل محیط دارد (انتساب دوباره بی‌اثر است):

{
  "success": false,
  "data": null,
  "errors": [{ "code": "ERR_NOT_FOUND_001", "message": "حساب بانکیِ بدون محیط یافت نشد" }]
}

POS devices

GET /api/v1/my/payment-methods/pos

List the card reader devices of the active environment (newest first), followed by any of the caller's own devices that still have no environment.

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "382eb554-5931-4f07-b1cc-53ade2597438",
      "bank_name": "ملت",
      "serial_number": "SN-98765",
      "terminal_number": "123456",
      "account_number": null,
      "is_active": true,
      "created_at": 1785238315,
      "entity_type": "clinic"
    }
  ]
}

POST /api/v1/my/payment-methods/pos

Create a POS device.

Body

Field Type Required Description
bank_name string Bank name
terminal_number string Terminal number
serial_number string Device serial number
account_number string Linked account number

Response 201

Single created record.

Errors

  • 422 ERR_VALIDATION_001bank_name or terminal_number missing (field set).

PUT /api/v1/my/payment-methods/pos/{uuid}

Update a POS device. Partial update; empty bank_name/terminal_number422.

Response 200

Updated record.

Errors

  • 404 ERR_NOT_FOUND_001 — uuid unknown or not owned.
  • 422 ERR_VALIDATION_001 — provided bank_name/terminal_number empty.

PATCH /api/v1/my/payment-methods/pos/{uuid}/status

Toggle is_active. No body.

Response 200

Record with flipped is_active.

Errors

  • 404 ERR_NOT_FOUND_001 — uuid unknown or not owned.

PATCH /api/v1/my/payment-methods/pos/{uuid}/environment

قرینهٔ endpoint انتساب حساب بانکی: کارتخوانِ بی‌محیطِ خودِ کاربر را به محیط فعال می‌چسباند. بدون بدنه.

Response 200

رکورد با entity_type پرشده.

Errors

  • 404 ERR_NOT_FOUND_001 — uuid ناشناس، مالِ کاربر دیگر، یا از قبل محیط دارد (پیام: «کارت خوانِ بدون محیط یافت نشد»).