defmodule DaProductApp.Merchants.Merchant do @moduledoc """ Unified merchant entity supporting both NPCI specification and partner management. Merchants are enrolled by partners and contain both NPCI-required fields and partner-specific business information for international payments. """ use Ecto.Schema import Ecto.Changeset alias DaProductApp.Merchants.MerchantInvoice alias DaProductApp.Partners.Partner alias DaProductApp.Transactions.Transaction schema "merchants" do # NPCI Required Fields (from UPI specification) field :mid, :string # Merchant ID (NPCI standard) field :tid, :string # Terminal ID field :sid, :string # Store ID field :sub_code, :string # Sub merchant code field :merchant_type, :string # SMALL, MEDIUM, LARGE field :merchant_genre, :string # RETAIL, ONLINE field :onboarding_type, :string # AGGREGATOR, DIRECT, BANK field :inst_code, :string # Institution code field :country_code, :string # Country code (SG, AE, US, IN) field :network_inst_id, :string # Network institution ID # Business Identity Fields (from international migration) field :merchant_code, :string # Internal unique identifier field :brand_name, :string # Public brand name field :legal_name, :string # Legal registered name field :franchise_name, :string # Franchise name if applicable field :ownership_type, :string # PRIVATE, PUBLIC, PARTNERSHIP # UPI & Payment Fields (from international migration) field :merchant_vpa, :string # UPI VPA (e.g., merchant@partner) field :business_type, :string # RETAIL, ECOMMERCE, SERVICES field :business_category, :string # MCC code category field :static_qr_code, :string # Static QR for merchant # Transaction Controls (from international migration) field :qr_enabled, :boolean, default: true field :dynamic_qr_enabled, :boolean, default: false field :max_transaction_limit, :decimal field :daily_transaction_limit, :decimal # International Support (new fields) field :corridor, :string # SINGAPORE, UAE, USA field :local_currency, :string # SGD, AED, USD, INR field :fx_markup_rate, :decimal # Partner's FX markup percentage # Settlement Details (from international migration + new) field :settlement_frequency, :string, default: "T+1" # T+0, T+1, WEEKLY field :settlement_account_intl, :string # From international migration field :settlement_account_number, :string # New field field :settlement_account_ifsc, :string # New field field :settlement_account_type, :string, default: "CURRENT" # New field field :settlement_bank_country, :string # New field # Contact Information (from international migration) field :contact_person, :string field :contact_email, :string field :contact_phone, :string field :address, :string field :city, :string field :state, :string field :pincode, :string # Compliance & KYC (from international migration + new) field :gstin, :string # GST identification number field :pan, :string # PAN card number field :regulatory_license, :string # New field field :compliance_status, :string, default: "PENDING" # New field field :risk_category, :string, default: "MEDIUM" # New field # Status & Tracking (from international migration + new) field :intl_status, :string, default: "ACTIVE" # From international migration field :status, :string, default: "ACTIVE" # New field (will replace intl_status) field :onboarded_at, :utc_datetime field :last_transaction_at, :utc_datetime field :partner_managed, :boolean, default: true # New field # Relationships belongs_to :partner, Partner, type: :binary_id has_many :invoices, MerchantInvoice has_many :transactions, Transaction timestamps(type: :utc_datetime) end @required ~w(mid tid)a @optional ~w( sid sub_code merchant_type merchant_genre onboarding_type inst_code brand_name legal_name franchise_name ownership_type country_code network_inst_id merchant_code merchant_vpa business_type business_category static_qr_code qr_enabled dynamic_qr_enabled max_transaction_limit daily_transaction_limit corridor local_currency fx_markup_rate settlement_frequency settlement_account_intl settlement_account_number settlement_account_ifsc settlement_account_type settlement_bank_country contact_person contact_email contact_phone address city state pincode gstin pan regulatory_license compliance_status risk_category intl_status status onboarded_at last_transaction_at partner_managed partner_id )a def changeset(struct, attrs) do struct |> cast(attrs, @required ++ @optional) |> validate_required(@required) |> validate_merchant_fields() |> validate_business_fields() |> validate_international_fields() |> validate_contact_fields() |> unique_constraint([:mid, :tid]) |> unique_constraint(:merchant_code) |> unique_constraint(:merchant_vpa) end # Validation helper functions defp validate_merchant_fields(changeset) do changeset |> validate_format(:mid, ~r/^[A-Za-z0-9]{6,15}$/, message: "MID must be 6-15 alphanumeric characters") |> validate_format(:tid, ~r/^[A-Za-z0-9]{1,8}$/, message: "TID must be 1-8 alphanumeric characters") |> validate_format(:merchant_code, ~r/^[A-Za-z0-9_]{4,20}$/, message: "Merchant code must be 4-20 characters") end defp validate_business_fields(changeset) do changeset |> validate_inclusion(:merchant_type, ["SMALL", "MEDIUM", "LARGE"]) |> validate_inclusion(:merchant_genre, ["RETAIL", "ONLINE"]) |> validate_inclusion(:onboarding_type, ["AGGREGATOR", "DIRECT", "BANK"]) |> validate_inclusion(:business_type, ["RETAIL", "ECOMMERCE", "SERVICES"]) |> validate_inclusion(:ownership_type, ["PRIVATE", "PUBLIC", "PARTNERSHIP"]) |> validate_format(:merchant_vpa, ~r/^[\w\.-]+@[\w\.-]+$/, message: "Invalid VPA format") |> validate_length(:brand_name, min: 2, max: 99) |> validate_length(:legal_name, max: 99) end defp validate_international_fields(changeset) do changeset |> validate_inclusion(:corridor, ["SINGAPORE", "UAE", "USA", "DOMESTIC"]) |> validate_inclusion(:local_currency, ["SGD", "AED", "USD", "INR"]) |> validate_inclusion(:country_code, ["SG", "AE", "US", "IN"]) |> validate_number(:fx_markup_rate, greater_than_or_equal_to: 0, less_than_or_equal_to: 10) |> validate_inclusion(:settlement_frequency, ["T+0", "T+1", "WEEKLY"]) |> validate_inclusion(:settlement_account_type, ["CURRENT", "SAVINGS"]) end defp validate_contact_fields(changeset) do changeset |> validate_format(:contact_email, ~r/@/, message: "Invalid email format") |> validate_format(:contact_phone, ~r/^\+?[\d\-\s\(\)]{7,15}$/, message: "Invalid phone format") |> validate_length(:pincode, is: 6) |> validate_inclusion(:compliance_status, ["PENDING", "VERIFIED", "REJECTED"]) |> validate_inclusion(:risk_category, ["LOW", "MEDIUM", "HIGH"]) |> validate_inclusion(:status, ["ACTIVE", "SUSPENDED", "INACTIVE"]) |> validate_inclusion(:intl_status, ["ACTIVE", "SUSPENDED", "INACTIVE"]) end # Helper functions for business logic def active?(merchant), do: (merchant.status || merchant.intl_status) == "ACTIVE" def international?(merchant), do: merchant.corridor && merchant.corridor != "DOMESTIC" def qr_enabled?(merchant), do: merchant.qr_enabled and active?(merchant) def can_process_amount?(merchant, amount) do active?(merchant) and (is_nil(merchant.max_transaction_limit) or Decimal.compare(amount, merchant.max_transaction_limit) != :gt) end end