cover/Elixir.DaProductApp.Merchants.Merchant.html

1 defmodule DaProductApp.Merchants.Merchant do
2 @moduledoc """
3 Unified merchant entity supporting both NPCI specification and partner management.
4
5 Merchants are enrolled by partners and contain both NPCI-required fields
6 and partner-specific business information for international payments.
7 """
8 use Ecto.Schema
9 import Ecto.Changeset
10
11 alias DaProductApp.Merchants.MerchantInvoice
12 alias DaProductApp.Partners.Partner
13 alias DaProductApp.Transactions.Transaction
14
15
:-(
schema "merchants" do
16 # NPCI Required Fields (from UPI specification)
17 field :mid, :string # Merchant ID (NPCI standard)
18 field :tid, :string # Terminal ID
19 field :sid, :string # Store ID
20 field :sub_code, :string # Sub merchant code
21 field :merchant_type, :string # SMALL, MEDIUM, LARGE
22 field :merchant_genre, :string # RETAIL, ONLINE
23 field :onboarding_type, :string # AGGREGATOR, DIRECT, BANK
24 field :inst_code, :string # Institution code
25 field :country_code, :string # Country code (SG, AE, US, IN)
26 field :network_inst_id, :string # Network institution ID
27
28 # Business Identity Fields (from international migration)
29 field :merchant_code, :string # Internal unique identifier
30 field :brand_name, :string # Public brand name
31 field :legal_name, :string # Legal registered name
32 field :franchise_name, :string # Franchise name if applicable
33 field :ownership_type, :string # PRIVATE, PUBLIC, PARTNERSHIP
34
35 # UPI & Payment Fields (from international migration)
36 field :merchant_vpa, :string # UPI VPA (e.g., merchant@partner)
37 field :business_type, :string # RETAIL, ECOMMERCE, SERVICES
38 field :business_category, :string # MCC code category
39 field :static_qr_code, :string # Static QR for merchant
40
41 # Transaction Controls (from international migration)
42 field :qr_enabled, :boolean, default: true
43 field :dynamic_qr_enabled, :boolean, default: false
44 field :max_transaction_limit, :decimal
45 field :daily_transaction_limit, :decimal
46
47 # International Support (new fields)
48 field :corridor, :string # SINGAPORE, UAE, USA
49 field :local_currency, :string # SGD, AED, USD, INR
50 field :fx_markup_rate, :decimal # Partner's FX markup percentage
51
52 # Settlement Details (from international migration + new)
53 field :settlement_frequency, :string, default: "T+1" # T+0, T+1, WEEKLY
54 field :settlement_account_intl, :string # From international migration
55 field :settlement_account_number, :string # New field
56 field :settlement_account_ifsc, :string # New field
57 field :settlement_account_type, :string, default: "CURRENT" # New field
58 field :settlement_bank_country, :string # New field
59
60 # Contact Information (from international migration)
61 field :contact_person, :string
62 field :contact_email, :string
63 field :contact_phone, :string
64 field :address, :string
65 field :city, :string
66 field :state, :string
67 field :pincode, :string
68
69 # Compliance & KYC (from international migration + new)
70 field :gstin, :string # GST identification number
71 field :pan, :string # PAN card number
72 field :regulatory_license, :string # New field
73 field :compliance_status, :string, default: "PENDING" # New field
74 field :risk_category, :string, default: "MEDIUM" # New field
75
76 # Status & Tracking (from international migration + new)
77 field :intl_status, :string, default: "ACTIVE" # From international migration
78 field :status, :string, default: "ACTIVE" # New field (will replace intl_status)
79 field :onboarded_at, :utc_datetime
80 field :last_transaction_at, :utc_datetime
81 field :partner_managed, :boolean, default: true # New field
82
83 # Relationships
84 belongs_to :partner, Partner, type: :binary_id
85 has_many :invoices, MerchantInvoice
86 has_many :transactions, Transaction
87
88 timestamps(type: :utc_datetime)
89 end
90
91 @required ~w(mid tid)a
92 @optional ~w(
93 sid sub_code merchant_type merchant_genre onboarding_type inst_code
94 brand_name legal_name franchise_name ownership_type country_code network_inst_id
95 merchant_code merchant_vpa business_type business_category static_qr_code
96 qr_enabled dynamic_qr_enabled max_transaction_limit daily_transaction_limit
97 corridor local_currency fx_markup_rate settlement_frequency settlement_account_intl
98 settlement_account_number settlement_account_ifsc settlement_account_type settlement_bank_country
99 contact_person contact_email contact_phone address city state pincode
100 gstin pan regulatory_license compliance_status risk_category intl_status status
101 onboarded_at last_transaction_at partner_managed partner_id
102 )a
103
104 def changeset(struct, attrs) do
105 struct
106 |> cast(attrs, @required ++ @optional)
107 |> validate_required(@required)
108 |> validate_merchant_fields()
109 |> validate_business_fields()
110 |> validate_international_fields()
111 |> validate_contact_fields()
112 |> unique_constraint([:mid, :tid])
113 |> unique_constraint(:merchant_code)
114
:-(
|> unique_constraint(:merchant_vpa)
115 end
116
117 # Validation helper functions
118 defp validate_merchant_fields(changeset) do
119 changeset
120 |> validate_format(:mid, ~r/^[A-Za-z0-9]{6,15}$/, message: "MID must be 6-15 alphanumeric characters")
121 |> validate_format(:tid, ~r/^[A-Za-z0-9]{1,8}$/, message: "TID must be 1-8 alphanumeric characters")
122
:-(
|> validate_format(:merchant_code, ~r/^[A-Za-z0-9_]{4,20}$/, message: "Merchant code must be 4-20 characters")
123 end
124
125 defp validate_business_fields(changeset) do
126 changeset
127 |> validate_inclusion(:merchant_type, ["SMALL", "MEDIUM", "LARGE"])
128 |> validate_inclusion(:merchant_genre, ["RETAIL", "ONLINE"])
129 |> validate_inclusion(:onboarding_type, ["AGGREGATOR", "DIRECT", "BANK"])
130 |> validate_inclusion(:business_type, ["RETAIL", "ECOMMERCE", "SERVICES"])
131 |> validate_inclusion(:ownership_type, ["PRIVATE", "PUBLIC", "PARTNERSHIP"])
132 |> validate_format(:merchant_vpa, ~r/^[\w\.-]+@[\w\.-]+$/, message: "Invalid VPA format")
133 |> validate_length(:brand_name, min: 2, max: 99)
134
:-(
|> validate_length(:legal_name, max: 99)
135 end
136
137 defp validate_international_fields(changeset) do
138 changeset
139 |> validate_inclusion(:corridor, ["SINGAPORE", "UAE", "USA", "DOMESTIC"])
140 |> validate_inclusion(:local_currency, ["SGD", "AED", "USD", "INR"])
141 |> validate_inclusion(:country_code, ["SG", "AE", "US", "IN"])
142 |> validate_number(:fx_markup_rate, greater_than_or_equal_to: 0, less_than_or_equal_to: 10)
143 |> validate_inclusion(:settlement_frequency, ["T+0", "T+1", "WEEKLY"])
144
:-(
|> validate_inclusion(:settlement_account_type, ["CURRENT", "SAVINGS"])
145 end
146
147 defp validate_contact_fields(changeset) do
148 changeset
149 |> validate_format(:contact_email, ~r/@/, message: "Invalid email format")
150 |> validate_format(:contact_phone, ~r/^\+?[\d\-\s\(\)]{7,15}$/, message: "Invalid phone format")
151 |> validate_length(:pincode, is: 6)
152 |> validate_inclusion(:compliance_status, ["PENDING", "VERIFIED", "REJECTED"])
153 |> validate_inclusion(:risk_category, ["LOW", "MEDIUM", "HIGH"])
154 |> validate_inclusion(:status, ["ACTIVE", "SUSPENDED", "INACTIVE"])
155
:-(
|> validate_inclusion(:intl_status, ["ACTIVE", "SUSPENDED", "INACTIVE"])
156 end
157
158 # Helper functions for business logic
159
:-(
def active?(merchant), do: (merchant.status || merchant.intl_status) == "ACTIVE"
160
:-(
def international?(merchant), do: merchant.corridor && merchant.corridor != "DOMESTIC"
161
:-(
def qr_enabled?(merchant), do: merchant.qr_enabled and active?(merchant)
162 def can_process_amount?(merchant, amount) do
163
:-(
active?(merchant) and
164
:-(
(is_nil(merchant.max_transaction_limit) or
165
:-(
Decimal.compare(amount, merchant.max_transaction_limit) != :gt)
166 end
167 end
Line Hits Source