- Project Overview
- Directory Structure
- Apps Description
- Models & Relationships
- Signals & Automation
- API Endpoints (Comprehensive)
- Serializers Summary
- Admin Panel & Management
- Templates & Email System
- Advanced Features & Implementation Details
- Project Configuration & Environment
- Type System & Constants
- Migrations & Database Schema
- Testing & Quality
- Deployment & Docker
- Common API Response Structure
- Future Improvements & Notes
- Troubleshooting & Common Issues
- File Organization & Best Practices
- Conclusion
- Name: ShadPay - Django-based banking application
- Purpose: Provide user account management, bank account handling, transactions with OTP verification, virtual cards, KYC/document handling, and admin tooling.
- Stack: Python, Django, Django REST Framework, Celery, RabbitMQ, Redis, djmoney, SQLite (dev) / PostgreSQL (prod).
(top-level — run tree -L 3 for full tree)
apps/: Django apps (account, cards, common)core/: Django project entry (ASGI/WSGI, settings, celery, urls)utils/: Helper functions (email, generate, path, validation, permissions, types)templates/: Email and frontend templatesstatic/: Static assetsmedia/: Uploaded files (avatars, KYC docs)requirements/: grouped requirementsDockerfile,docker-compose.yml,manage.pyetc.
-
Account App (
apps/account): Core user and account management- Models:
User,Settings,Profile,BankAccount,Currency,BankAccountType,Transaction,AccountRecharge,KYC,KYCDocument. - APIs: profile, settings, bank account, account recharge, KYC, transactions, authentication endpoints (OTP, activation token).
- Signals: create related
ProfileandSettingson user creation; createBankAccountside-effects (VirtualCard, KYC); set balances and send emails for account recharge; KYC lifecycle updates; transaction OTP completion updates balances and sends notifications.
- Models:
-
Cards App (
apps/cards): Virtual card generation and card-to-card transfers- Models:
VirtualCard(linked toBankAccount). - APIs: list virtual cards, card-to-card transfer endpoint.
- Models:
-
Common App (
apps/common): Shared models and utilities- Models:
BaseModel(abstract base withUUID id,created_at,updated_at),ContentView.
- Models:
Below are the main models, field summaries, relationships, constraints and indexes discovered in apps/*/models.
-
User(apps/account/models/user.py)- Fields:
id(Char pk, uuid default),unique_id(Char),email(EmailField, unique),is_active,is_staff,is_verified,token_used, OTP fields:otp_code,otp_expire_time,otp_used,activation_token,date_joined. - Manager:
UserManagerwithcreate_userandcreate_superuser. - Indexes:
emailindex. - Methods:
set_otp_code(),verify_otp_code(otp),set_activation_token(),verify_activation_token(token)— these send emails viautils.email.send_email.
- Fields:
-
Settings(apps/account/models/settings.py)- Fields:
user(OneToOne ->User),role(choices),status(choices),is_notification_enabled. - Indexes:
user. - Notes: created automatically on
Usercreation by signal.
- Fields:
-
Profile(apps/account/models/profile.py)- Fields:
user(OneToOne ->User),avatar(ImageField upload viautils.path.avatar.user_profile_avatar_upload_path),first_name,last_name,gender,birthday. - Validators: image size and extension via
utils.validation.base_validator. - On save: deletes previous avatar file when replaced (transactional).
- Fields:
-
Currency(apps/account/models/bank_account.py)- Fields:
code(unique),name,symbol,is_active,rate_to_usd(MoneyField).
- Fields:
-
BankAccountType- Fields:
code(unique),name,description,is_active.
- Fields:
-
BankAccount- Fields:
user(FK ->User),currency(FK ->Currency, SET_NULL),account_type(FK ->BankAccountType, SET_NULL),account_number(Char unique, default generator),account_balance(MoneyField),status,is_primary(bool),kyc_status,verified_at,verification_notes. - Indexes: status, account_type, (user, kyc_status), (user, account_number).
- On save: when
is_primary=True, updates other bank accounts for the user tois_primary=False(atomic transaction).
- Fields:
-
Transaction- Fields:
sender(FK ->User),receiver(FK ->User),initiated_by(FK ->User),sender_account(FK ->BankAccount),receiver_account(FK ->BankAccount),initiated_by_card(FK ->VirtualCard, nullable),approved_by(FK ->User, nullable),approved_at,description,amount(MoneyField),status(choices),transaction_type(choices),reference_code(unique), OTP fields:otp_code,otp_expire_time,otp_used. - Indexes: many, including status, sender, receiver, accounts, reference_code, created_at, approved_by/at.
- Validation:
clean()enforces positive amount, restrictions by transaction type, and sufficient balance. - OTP:
set_otp_code()generates OTP, emails sender viasend_transaction_otp_code;verify_otp_code()checks expiry and usage.
- Fields:
-
AccountRecharge- Fields:
user(FK ->User),bank_account(FK ->BankAccount),money_requested(MoneyField),status(choices),rejection_status,rejection_description. - Indexes: user, status, bank_account, rejection_status.
save()enforces the requestor is owner of the bank account; post-save signal updates bank account balance whenstatus == APPROVEDand triggers email notifications.
- Fields:
-
KYCandKYCDocument(apps/account/models/kyc_document.py)KYC:user(FK),bank_account(FK),status,kyc_status,notes. Indexes andunique_together = ('user','bank_account').KYCDocument:kyc(FK),document_file(FileField upload viautils.path.kyc_document.user_kyc_document_upload_path),document_type,status,notes. Unique per(kyc, document_type).- Signals: when
KYC.kyc_statuschanges toVERIFIED/PENDING/REJECTED/NOT_SUBMITTED, correspondingBankAccountstatus and kyc_status are updated and timestamps/notes are saved.
-
VirtualCard(apps/cards/models/virtual_card.py)- Fields:
bank_account(FK ->BankAccount),card_number(Char unique),cvv,expire_month,expire_year,status,daily_limit,monthly_limit(MoneyField). - On BankAccount creation a
VirtualCardis created by a signal; on save, expiry month/year are generated if missing (viautils.generate.expire_time.get_random_expiry).
- Fields:
apps/account/signals/create_profile.py:post_saveonUsercreates aProfile.apps/account/signals/create_settings.py:post_saveonUsercreates aSettingsrow.apps/account/signals/create_bank_account.py:post_saveonBankAccountcreates aVirtualCard, aKYCentry, and sends bank-account-created email.apps/account/signals/account_recharge.py:post_saveonAccountRechargewill add money toBankAccount.account_balancewhenstatus==APPROVEDand sends status email.apps/account/signals/kyc_documents.py:post_saveonKYCupdatesBankAccountstatus/kyc_status based onKYC.kyc_statusand persists notes and verified_at when appropriate.apps/account/signals/verify_otp_transaction_code.py:post_saveonTransactionwhenstatus==COMPLETEDadjusts sender/receiver balances and sends notification emails.
API Endpoints (Comprehensive)
All API endpoints are mounted under api/v1/ (see core/urls.py). Full endpoint and view details:
-
SendOTPCodeAPIView(apps/account/api/v1/views/send_otp_code.py)- Endpoint:
POST /api/v1/account/auth/token/send-otp-code/ - Purpose: Initiate OTP login/registration flow.
- Request:
{ "email": "user@example.com" } - Response (existing user):
{ "error": false, "detail": "Success: OTP code sent to your email.", "code": 1001 } - Response (new user):
{ "error": false, "detail": "Success: OTP code sent for registration.", "code": 1002 } - Logic: If email exists, send OTP; if not, create user and send OTP.
- Endpoint:
-
VerifyOTPCodeAPIView(apps/account/api/v1/views/verify_otp_code.py)- Endpoint:
POST /api/v1/account/auth/token/verify-otp-code/ - Purpose: Verify OTP and return JWT tokens (access + refresh).
- Request:
{ "email": "user@example.com", "otp_code": 123456 } - Response (success):
{ "error": false, "detail": "JWT Token generated.", "access": "<jwt>", "refresh": "<jwt>", "code": 1003 } - Response (invalid OTP):
{ "error": true, "detail": "Verify otp code is expired or invalid.", "code": 2003 } - Logic: Calls
user.verify_otp_code()anduser.set_activation_token(), then generates JWT tokens viaRefreshToken.for_user(user).
- Endpoint:
-
SendActivationTokenAPIView(apps/account/api/v1/views/send_activation_token.py)- Endpoint:
POST /api/v1/account/auth/token/send-activation-token/ - Permission:
IsAuthenticated - Purpose: Resend activation token if account not yet verified.
- Request: Empty body (authentication via Bearer token)
- Response (success):
{ "error": false, "detail": "Send Activation Token for your Mail Address.", "code": 1004 } - Response (already verified):
{ "error": true, "detail": "Your account has already been verified.", "code": 2010 } - Logic: Checks
user.token_usedand callsuser.set_activation_token()if False.
- Endpoint:
-
VerifyActivationTokenAPIView(apps/account/api/v1/views/verify_activation_token.py)- Endpoint:
POST /api/v1/account/auth/token/verify-activation-token/ - Permission:
IsAuthenticated - Purpose: Verify email address and finalize account activation.
- Request:
{ "activation_token": "<token>" } - Response (success):
{ "error": false, "detail": "Activation Token is Successful.", "code": 1005 } - Response (invalid):
{ "error": true, "detail": "Your Token is Expired or Invalid.", "code": 2007 } - Logic: Calls
user.verify_activation_token(token), settingis_verified = True.
- Endpoint:
-
LogoutView(apps/account/api/v1/views/user_logout.py)- Endpoint:
GET|POST /api/v1/account/auth/token/logout/(mapped in admin URLs) - Purpose: Logout authenticated user (session-based).
- Logic: Calls Django's
logout(request)and redirects to/.
- Endpoint:
-
ProfileViewSet(apps/account/api/v1/views/profile.py)- Endpoints:
GET|PUT|PATCH /api/v1/account/profile/ - Permission:
IsAuthenticated - Serializer:
ProfileSerializer— fields:id,avatar,first_name,last_name,gender,birthday,created_at - Behavior: User can only view/edit their own profile.
- Endpoints:
-
SettingsViewSet(apps/account/api/v1/views/settings.py)- Endpoints:
GET|PUT|PATCH /api/v1/account/settings/ - Permission:
IsAuthenticated - Serializer:
SettingsSerializer— fields:id,role,status,is_notification_enabled,created_at - Behavior: User can only view/edit their own settings. Role-based permissions (
BRANCH_MANAGER,ACCOUNT_EXECUTIVE,TELLER,CUSTOMER) are checked elsewhere.
- Endpoints:
-
CurrencyViewSet(apps/account/api/v1/views/bank_account.py)- Endpoints:
GET /api/v1/account/bank-account-currency/ - Serializer:
CurrencySerializer— fields:id,code,name,symbol,rate_to_usd - Behavior: List only active currencies.
- Endpoints:
-
BankAccountTypeViewSet- Endpoints:
GET /api/v1/account/bank-account-type/ - Serializer:
BankAccountTypeSerializer— fields:id,code,name,description - Behavior: List only active account types.
- Endpoints:
-
BankAccountViewSet- Endpoints:
GET|POST /api/v1/account/bank-account/ - Permission:
IsAuthenticated - Serializer (GET):
GetBankAccountSerializer— includes related currency and account_type objects - Serializer (POST):
PostBankAccountSerializer— user is auto-attached inperform_create() - Behavior: User can only view/create their own bank accounts. On create, a signal triggers
VirtualCardandKYCcreation and sends confirmation email.
- Endpoints:
AccountRechargeViewSet(apps/account/api/v1/views/account_recharge.py)- Endpoints:
GET|POST /api/v1/account/account-recharge/ - Permission:
IsAuthenticated - Serializer:
AccountRechargeSerializer— fields:id,bank_account,money_requested,status,rejection_status,rejection_description,created_at,updated_at(some read-only) - Behavior:
perform_create()validates that user owns the bank account. On approval (via admin), a signal updatesbank_account.account_balanceand sends status email.
- Endpoints:
-
KYCViewSet(apps/account/api/v1/views/kyc_document.py)- Endpoints:
GET /api/v1/account/kyc/ - Permission:
IsAuthenticated - Serializer:
KYCSerializer— includes nestedkyc_documentarray - Behavior: User can only view their own KYC entries. Read-only (no POST allowed).
- Endpoints:
-
KYCDocumentViewSet- Endpoints:
GET|POST /api/v1/account/kyc-document/ - Permission:
IsAuthenticated - Serializer:
KYCDocumentSerializer— fields:id,kyc,document_file,document_type,status,notes(status & notes read-only) - Behavior: User can upload KYC documents; admin reviews and updates status via admin panel, triggering KYC lifecycle signal.
- Endpoints:
-
TransactionViewSet(apps/account/api/v1/views/transaction_request.py)- Endpoints:
GET|POST /api/v1/account/transaction/ - Permission:
IsAuthenticated - Serializer (GET):
GetTransactionSerializer— detailed transaction info - Serializer (POST):
PostTransactionSerializer— fields:sender_account,receiver_account,amount,description,transaction_type - Behavior: User can view transactions where they are sender, receiver, or initiator.
perform_create()sets sender/receiver/initiator and callstransaction.set_otp_code()which sends OTP email.
- Endpoints:
-
VerifyOTPTransactionCodeAPIView(apps/account/api/v1/views/transaction_verify.py)- Endpoint:
POST /api/v1/account/transaction/verify-otp-code/ - Permission:
IsAuthenticated - Purpose: Verify transaction OTP and finalize transfer.
- Request:
{ "transaction_id": "<uuid>", "otp_code": 123456 } - Response (success):
{ "error": false, "detail": "Transaction verified successfully.", "code": 1001 } - Logic: Calls
transaction.verify_otp_code(), updates status toCOMPLETED, setsapproved_byandapproved_at, and triggers signal to move funds and send notifications.
- Endpoint:
-
ResendOTPTransactionCodeAPIView- Endpoint:
POST /api/v1/account/transaction/resend-otp-code/ - Permission:
IsAuthenticated - Purpose: Resend OTP for a pending transaction.
- Request:
{ "transaction_id": "<uuid>" } - Logic: Fetches transaction, calls
transaction.set_otp_code(), and sends new OTP email.
- Endpoint:
-
VirtualCardViewSet(apps/cards/api/v1/views/virtual_card.py)- Endpoints:
GET /api/v1/cards/virtual-cards/ - Permission:
IsAuthenticated - Serializer:
VirtualCardSerializer— includes nested bank account, card_number, cvv, expiry, status, daily/monthly limits - Behavior: User can only view virtual cards linked to their bank accounts.
- Endpoints:
-
CardToCardTransferAPIView(apps/cards/api/v1/views/card_to_card_transfer.py)- Endpoint:
POST /api/v1/cards/cart-to-card-transfer(note: typo "cart" instead of "card") - Permission:
IsAuthenticated - Purpose: Transfer money between virtual cards with OTP verification.
- Request:
{ "recipient_card": "<card_number>", "sender_card": "<optional>", "amount": { "amount": 100, "currency": "USD" } } - Response (success):
{ "success": true, "transaction_id": "<uuid>", "message": "OTP sent to your email..." } - Logic:
- Validates recipient card exists.
- If sender_card provided, uses that card's bank account; otherwise uses user's primary bank account.
- Validates currency match and sufficient balance.
- Creates
Transactionobject, callsset_otp_code(), and returns transaction ID.
- Notes: Transaction must still be verified via
transaction/verify-otp-code/endpoint.
- Endpoint:
Serializers Summary
All serializers are in apps/*/api/v1/serializers/:
-
Account Serializers:
ProfileSerializer: user profile dataSettingsSerializer: user role, status, notificationsCurrencySerializer: currency codes and exchange ratesBankAccountTypeSerializer: account type metadataGetBankAccountSerializer/PostBankAccountSerializer: bank account CRUDTransactionSerializer(GET & POST variants): transaction detailsKYCSerializer/KYCDocumentSerializer: KYC document submissionAccountRechargeSerializer: account recharge requests
-
Card Serializers:
VirtualCardSerializer: card details with nested bank account
All serializers use DRF's ModelSerializer and include appropriate read_only_fields and fields declarations.
ShadPay uses Django's admin with the unfold package (modern admin UI) and admin_honeypot (security). Real admin is behind a variable URL prefix; fake honeypot at /admin/.
apps/account/admin/user.py: Manage users, view OTP codes and activation tokens, mark verified/unverified.apps/account/admin/profile.py: View/edit user profiles, avatars, personal data.apps/account/admin/settings.py: Manage user roles, account status, notification settings.apps/account/admin/bank_account.py: View bank accounts, update KYC status, manage account types and currencies.apps/account/admin/account_recharge.py: Review recharge requests, approve/reject with reasons (triggers signal to update balance).apps/account/admin/kyc_document.py: Review and approve KYC documents; updates trigger lifecycle signal.apps/account/admin/key_document.py: (Likely for admin document management)apps/cards/admin/virtual_card.py: View virtual cards, update status, manage limits.
apps/account/admin/honeypot.pyregistersLoginAttemptmodel with enhanced display:- Displays: username (color-coded), IP address (clickable), timestamp (human-readable), threat level, browser type, attempted path.
- Threat Analysis: Color-coded risk levels (🔴 HIGH, 🟠 MEDIUM, 🟢 LOW) based on:
- Common attack usernames (admin, root, etc.)
- Suspicious user agents (bots, crawlers)
- Repeated attempts from same IP.
- Actions: Mark reviewed, block IPs, export to CSV.
- Security Analysis: Detailed breakdown of detected threats, including IP geolocation stub (for future implementation).
- Read-only: No add/delete permissions; view-only for security auditing.
- Real admin behind
ADMIN_URL_PREFIXenv var (default: "real-admin" in dev). - Fake
/admin/honeypot to trap attackers. django-axesbrute-force protection: locks after 5 failures (configurable), 1-hour cooloff, lockout template.- Session timeout: 1 week expiry on inactivity.
All email templates are in templates/email/:
send_otp_code.html: OTP code with 2-minute expiry (sent byUser.set_otp_code()).send_activation_link.html: Account activation link with token (sent byUser.set_activation_token()).bank_account_created.html: Confirms new bank account with account number and initial balance (sent bycreate_bank_accountsignal).account_recharge_status.html: Notifies user of recharge approval/rejection (sent byaccount_recharge_send_emailsignal).transaction_otp_email.html: OTP for transaction verification (sent byTransaction.set_otp_code()).transaction_notification_email.html: Confirms transaction completion, showing debit/credit (sent byverify_otp_transaction_codesignal).
All email functions in utils/email/send_email.py:
send_otp_code(email, otp_code, expire_time): Sends OTP for login/registration.send_activation_email(email, activation_token): Sends activation link.send_bank_account_information(instance): Sends bank account creation confirmation.send_account_recharge_status_email(instance): Sends recharge request status.send_transaction_otp_code(...): Sends transaction OTP with details.send_transaction_notification(...): Sends transaction completion notification.
Note: Email sending is synchronous (blocks request). For production, consider migrating to Celery tasks (see "Future Improvements").
static/img/: Logo, favicon, UI assets.static/js/main.js: Frontend utilities.static/style/: CSS for admin and public site.templates/account/locked.html: Shown when user is locked out bydjango-axes.templates/admin_honeypot/login.html: Fake admin login page (part ofadmin_honeypotpackage).
- Request:
POST /api/v1/account/transaction/with sender/receiver accounts and amount. - Validation:
Transaction.clean()validates amount > 0, accounts exist, currency match, sufficient balance. - OTP Generation:
perform_create()callstransaction.set_otp_code():- Generates 6-digit OTP.
- Sets expiry to now + 2 minutes.
- Emails OTP to sender via
send_transaction_otp_code().
- Verification:
POST /api/v1/account/transaction/verify-otp-code/with transaction_id and OTP:- Verifies OTP (value + expiry).
- Sets transaction status to
COMPLETED. - Records
approved_by(current user) andapproved_at(timestamp).
- Balance Update: Signal
verify_otp_transaction_codetriggers:- Debits
sender_account.account_balanceby amount. - Credits
receiver_account.account_balanceby amount. - Sends notifications to both parties.
- Debits
- Account Creation: When
BankAccountis created, signal createsKYC(statusSUSPENDED, kyc_statusNOT_SUBMITTED). - Document Upload: User uploads
KYCDocument(s) viaPOST /api/v1/account/kyc-document/. - Admin Review: Admin updates
KYC.kyc_statustoPENDING/VERIFIED/REJECTEDvia admin panel. - Signal Update:
kyc_documentssignal updates:- If
VERIFIED:BankAccount.status = ACTIVE,kyc_status = VERIFIED, setsverified_at,verification_notes. - If
PENDING:BankAccount.status = SUSPENDED,kyc_status = PENDING. - If
REJECTED:BankAccount.status = INACTIVE,kyc_status = REJECTED, sets notes. - If
NOT_SUBMITTED:BankAccount.status = SUSPENDED.
- If
- Generation:
utils/generate/unique_code.py::generate_otp_code()→ 6-digit random int (100000-999999). - Storage: Stored on
UserorTransactionmodel. - Expiry: 2 minutes from generation.
- Verification:
User.verify_otp_code(otp)andTransaction.verify_otp_code(otp)both check time + value match. - Consumption: Set
otp_used = Trueonce verified (prevents replay).
- Avatar:
utils/path/avatar.py::user_profile_avatar_upload_path()→ stored inmedia/profile/avatar/with NanoID prefix. - KYC Documents:
utils/path/kyc_document.py::user_kyc_document_upload_path()→ stored inmedia/kyc/document/with NanoID prefix. - Validators (in
utils/validation/base_validator.py):validate_file_size_volume(): max ~5MB (5000 KB).validate_image_file_extension():.jpg,.jpeg,.png,.webp,.heic,.heif,.heifs.validate_kyc_file_extension():.jpg,.jpeg,.png,.pdf,.bmp,.tiff.
Roles defined in utils/type/account/settings_type.py:
BRANCH_MANAGER: High-level permissions (not yet fully implemented in views).ACCOUNT_EXECUTIVE: Account operations.TELLER: Basic transactions.CUSTOMER: Self-service only.
Permission classes in utils/permissions/account.py: IsBranchManagerPermission, IsAccountExecutivePermission, IsTellerPermission, IsCustomerPermission.
Currencymodel stores code (USD, EUR, etc.), name, symbol, andrate_to_usd(MoneyField).- All transactions and account balances use
djmoney.MoneyField(amount + currency). - When creating transactions, currency mismatch between sender/receiver accounts triggers validation error.
core/settings/common.py: Shared config (apps, middleware, logging, email, caching, Celery, JWT, admin).core/settings/development.py: Debug = True, SQLite database, Django Debug Toolbar.core/settings/production.py: Debug = False, PostgreSQL database, environment variables (SECRET_KEY, ALLOWED_HOSTS, DB credentials).
# Django
SECRET_KEY=<strong-secret>
DJANGO_SETTINGS_MODULE=core.settings.production
# JWT
JWT_SECRET_KEY=<strong-secret>
# Email
EMAIL_HOST=<smtp-host>
EMAIL_PORT=<port>
EMAIL_HOST_USER=<username>
EMAIL_HOST_PASSWORD=<password>
DEFAULT_FROM_EMAIL=<sender-email>
# Database (production)
POSTGRES_DB=shadpay_db
POSTGRES_USER=shadpay_user
POSTGRES_PASSWORD=<password>
POSTGRES_HOST=db
POSTGRES_PORT=5432
# Cache & Broker
REDIS_URL=redis://redis:6379/0
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672//
# Security
ADMIN_URL_PREFIX=<secret-admin-path>
ALLOWED_HOSTS=example.com,www.example.com
# API
SITE_URL=https://example.com
SITE_NAME=ShadPayConfigured in common.py with file-based handlers:
tmp/logs/debug.log: DEBUG leveltmp/logs/info.log: INFO leveltmp/logs/warning.log: WARNING leveltmp/logs/error.log: ERROR leveltmp/logs/critical.log: CRITICAL level
Redis cache backend configured at REDIS_URL for session and query caching.
- Broker: RabbitMQ (
RABBITMQ_URL) - Result Backend: Redis (
REDIS_URL) - Current Tasks: None defined (placeholder
beat_schedule) - Recommendation: Move email sending to Celery tasks for async processing.
Type choices/enums in utils/type/:
-
Account Types (
utils/type/account/):RoleType: BRANCH_MANAGER, ACCOUNT_EXECUTIVE, TELLER, CUSTOMERStatusType: ACTIVE, SUSPENDED, INACTIVEGenderType: (defined in models)AccountStatusType: ACTIVE, SUSPENDED, INACTIVEKYCStatusType: NOT_SUBMITTED, PENDING, VERIFIED, REJECTEDTransactionType: TRANSFER, DEPOSIT, WITHDRAWALTransactionStatusType: PENDING, COMPLETED, FAILED, CANCELLEDAccountRechargeStatusType: PENDING, APPROVED, REJECTEDAccountRechargeRejectionReasonType: (various reasons)
-
Card Types (
utils/type/cards/):VirtualCardStatusType: ACTIVE, INACTIVE, BLOCKED, EXPIRED
Key migrations in apps/account/migrations/:
0001_initial.py: Initial schema (User, Profile, Settings, BankAccount, Transaction, AccountRecharge, KYC).0002_alter_bankaccount_account_balance.py: MoneyField configuration.0003_transaction_otp_code_...: Added OTP fields to Transaction.0004-0006_alter_transaction_initiated_by_...: Relation tweaks.0007_transaction_initiated_by_card.py: Virtual card relation.
Migration strategy: Always run python manage.py migrate when deploying.
Tests are stubbed in apps/*/tests/__init__.py. Recommend adding:
- Unit tests for OTP generation and verification.
- Integration tests for transaction lifecycle.
- API endpoint tests for all viewsets.
- Permission tests for role-based access.
Run tests:
python manage.py test- Dockerfile: Multi-stage build with Python dependencies, static file collection, migrations.
- docker-compose.yml: Services for web (Django), db (PostgreSQL), redis, rabbitmq.
- nginx.conf: Reverse proxy configuration in
docker/nginx/.
Example docker-compose run:
docker-compose up -dAll API responses follow a consistent JSON structure:
{
"error": false,
"detail": "Success message",
"code": 1001,
"data": {}
}or on error:
{
"error": true,
"detail": "Error message",
"code": 2001,
"detail-error": "Technical error details"
}| Code | Context | Meaning |
|---|---|---|
| 1001-1005 | Authentication | Success codes |
| 2001-2025 | Authentication/Validation | Client errors |
| 7001 | Server | Server errors |
| 4001-4004 | Cards | Card transfer validation |
- Async Email: Migrate
utils.email.send_emailfunctions to Celery tasks for reliability and performance. - Pagination: Most endpoints return paginated lists (default page size = 10).
- Rate Limiting: Currently throttled to 5 requests/day for anonymous and 5 for authenticated (adjust in settings if needed).
- IP Geolocation: Honeypot admin has stub for IP geolocation (can integrate with ipapi.co or ipinfo.io).
- Transaction Limits: Virtual cards have daily/monthly limits that can be enforced in views.
- Currency Conversion: Exchange rates stored but not yet used in conversion logic.
- Audit Trail: Consider adding admin action logs for sensitive operations.
- 2FA: Second factor authentication not yet implemented (OTP is single factor).
- OTP not arriving: Check EMAIL_* env vars and SMTP settings in
common.py. - Database migration errors: Run
python manage.py makemigrationsandpython manage.py migrate. - Permission denied on endpoint: Verify user role in admin panel and check permission class decorators.
- Card not found errors: Ensure virtual card exists and user owns the bank account.
- Transaction fails with "Insufficient funds": Check
sender_account.account_balance.
apps/account/
models/ # Data models (User, BankAccount, Transaction, etc.)
admin/ # Admin panel registrations
api/v1/
views/ # API viewsets and APIView classes
serializers/ # DRF serializers
urls.py # URL routing
signals/ # Django signals (auto-triggers)
tests/ # Unit tests
migrations/ # Database schema
utils/
email/ # Email sending helpers
generate/ # ID and token generators
path/ # File upload path helpers
validation/ # File and data validators
permissions/ # DRF permission classes
type/ # Type choices and enums
docs/ # API documentation (Swagger setup)
core/
settings/ # Django settings (common, dev, prod)
management/ # Management commands
celery.py # Celery configuration
urls.py # Root URL routing
wsgi.py / asgi.py # Application entry points
templates/ # HTML templates (email, admin)
static/ # CSS, JS, images
media/ # User uploads (avatars, KYC docs)
ShadPay is a comprehensive Django banking application with:
- ✅ Custom user authentication via OTP and JWT
- ✅ Multi-account bank system with KYC verification
- ✅ Secure transaction processing with OTP confirmation
- ✅ Virtual card generation and management
- ✅ Admin honeypot and brute-force protection
- ✅ Role-based access control
- ✅ Email notifications for all critical events
- ✅ Docker containerization for deployment
For new developers:
- Read this doc and model diagrams.
- Run
docker-compose upto start services. - Create a superuser:
python manage.py createsuperuser. - Visit
http://localhost:8000/<ADMIN_URL_PREFIX>/to access admin. - Use Swagger/Scalar at
http://localhost:8000/api/v1/docs/for API exploration.
Utils & Helpers
-
utils/generate/unique_code.py— random generators:unique_small_string()andunique_big_string()— short human-readable IDs.generate_account_number()— returnsShadPay...-prefixed account numbers.generate_otp_code()— 6-digit OTP.generate_activation_token()— url-safe token viasecrets.token_urlsafe(32).generate_virtual_card_number()andgenerate_virtual_card_cvv2()— card identifiers.
-
utils/generate/expire_time.py—get_random_expiry()used byVirtualCardto pick expire month/year. -
utils/email/send_email.py— centralized email helpers that render templates and send OTP, activation, bank-account, recharge, and transaction emails. -
utils/path/*— upload path helpers:avatar.user_profile_avatar_upload_path— names avatars with NanoID prefix.kyc_document.user_kyc_document_upload_path— names KYC uploads with NanoID prefix.
-
utils/validation/base_validator.py— file validators used byProfile.avatarandKYCDocument.document_file:validate_file_size_volume()enforces < ~5MB limit.validate_image_file_extension()andvalidate_kyc_file_extension()enforce allowed extensions.
-
utils/permissions/account.py— role-based DRF permissions:IsBranchManagerPermission,IsAccountExecutivePermission,IsTellerPermission,IsCustomerPermission(based onuser.user_settings.role).
Settings & Configuration
- Main settings entry:
core/settings/common.pywith environment-aware overrides indevelopment.pyandproduction.py. - Important configurations:
AUTH_USER_MODEL = "account.User"(custom user).REST_FRAMEWORKusesJWTAuthenticationfromrest_framework_simplejwt(configured inSIMPLE_JWTwithACCESS_TOKEN_LIFETIME= 7 days, algorithmHS512).EMAIL_BACKENDand SMTP env vars used inutils.email.send_email.AXESandadmin_honeypotpackages for admin security;AXESlockout and template configured (locked.html).CACHESconfigured to use Redis (REDIS_URLenv var).Celeryis configured (core/celery.py) to use RabbitMQ (RABBITMQ_URL) and Redis result backend.
Security, Validation & Permission Layers
- Authentication: JWT via
rest_framework_simplejwt(tokens returned by OTP verify view).SIMPLE_JWTsettings control token expiry and signing key via envJWT_SECRET_KEY. - Brute-force protection:
django-axesconfigured withAXES_FAILURE_LIMITandAXES_COOLOFF_TIMEandLOCKOUT_TEMPLATE. - Admin protection:
admin_honeypotis installed and the real admin is behindADMIN_URL_PREFIX(changeable); fakeadmin/honeypot included. - OTP & Activation
- OTP generation:
generate_otp_code()creates a 6-digit int. - User OTP lifecycle:
User.set_otp_code()setsotp_code,otp_expire_time = now + 2 minutes,otp_used=False, and sends OTP via email (commented inuser.set_otp_code()but used elsewhere). - Transaction OTP:
Transaction.set_otp_code()does similarly and emails viasend_transaction_otp_code(). - Verification endpoints check both OTP value and expiry and set
otp_used=Truewhen consumed.
- OTP generation:
- Role-based permissions:
utils.permissions.accountprovides DRF permission classes referencinguser.user_settings.role(enum inutils/type/*). - File upload validation:
utils.validation.base_validatorensures file size and allowed extensions.
Examples & Code Snippets
- Creating a user and sending OTP (via API):
curl -X POST "http://localhost:8000/api/v1/account/auth/token/send-otp-code/" \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com"}'- Creating a transaction (authenticated request):
POST /api/v1/account/transaction/ HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
{
"sender_account": "<bank_account_id>",
"receiver_account": "<receiver_bank_account_id>",
"amount": "100.00",
"transaction_type": "TRANSFER",
"description": "Payment for invoice #123"
}Server behavior:
-
perform_create()inTransactionViewSetsetssender,initiated_by,receiverand callsset_otp_code()which generates OTP and emails the sender. UsePOST /transaction/verify-otp-code/to confirm. -
Verifying OTP for a transaction (example payload):
POST /api/v1/account/transaction/verify-otp-code/
Authorization: Bearer <access_token>
Content-Type: application/json
{
"transaction_id": "<transaction_uuid>",
"otp_code": 123456
}Migrations & Schema Notes
- Migrations exist under
apps/*/migrations/with incremental changes; notable model evolutions include transaction OTP fields and relation changes (see migration filenames inapps/account/migrations/).
Celery & Management Commands
core/celery.pysets up Celery, usingRABBITMQ_URLandREDIS_URLfrom env. No scheduled tasks defined in repo (placeholderbeat_schedule).- Management command:
core/management/commands/startapp.py(utility for scaffolding new apps).
Notes / Important Details
- Email sending is synchronous in
utils.email.send_email— consider moving to Celery tasks for reliability and performance in production. - OTP is short-lived (2 minutes) — ensure client UX supports rapid delivery and retry/resend endpoints exist for transactions.
- Sensitive defaults are present in development settings (SQLite, DEBUG=True). Ensure production environment variables are set and
SECRET_KEYandJWT_SECRET_KEYare strong and not checked into VCS. - File upload size limits and extensions are validated — double-check storage backend (S3 or similar) in production if needed.
