Files
customerTicket/apps/payments/models.py
T
2026-10-06 12:30:28 +03:30

236 lines
12 KiB
Python

"""Financial records are independent of tickets; issued instructions and evidence are snapshots."""
import uuid
from pathlib import Path
from django.conf import settings
from django.db import models
from apps.core.models import PublicIdModel, TimeStampedModel
MAX_AMOUNT = 9_007_199_254_740_991
class BankInstitution(models.Model):
code = models.SlugField(unique=True)
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
historical = models.BooleanField(default=False)
class Meta:
ordering = ['historical', 'name']
def __str__(self):
return self.name
class CompanyBankAccount(PublicIdModel, TimeStampedModel):
name = models.CharField('نام نمایشی حساب', max_length=120)
bank = models.ForeignKey(BankInstitution, on_delete=models.PROTECT, verbose_name='بانک')
holder = models.CharField('صاحب حساب', max_length=150)
account_number = models.CharField('شماره حساب', max_length=40, blank=True)
iban = models.CharField('شبا', max_length=26, blank=True)
card_number = models.CharField('شماره کارت', max_length=16, blank=True)
requires_deposit_id = models.BooleanField('شناسه واریز الزامی', default=False)
is_active = models.BooleanField('فعال کلی', default=True)
available_to_staff = models.BooleanField('قابل انتخاب کارشناسان', default=True)
available_to_customers = models.BooleanField('قابل انتخاب مشتریان', default=True)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['sort_order', 'name', 'pk']
permissions = [('manage_company_bankaccounts', 'Manage company bank accounts')]
def __str__(self):
return self.name
def snapshot(self):
return {key: getattr(self, key) for key in ('name', 'holder', 'account_number', 'iban', 'card_number', 'requires_deposit_id')} | {'bank': self.bank.name, 'id': str(self.public_id)}
class PaymentAccessConfiguration(models.Model):
singleton = models.PositiveSmallIntegerField(default=1, unique=True, editable=False)
open_view = models.BooleanField('مشاهده برای همه کارشناسان فعال', default=True)
open_create = models.BooleanField('ایجاد برای همه کارشناسان فعال', default=True)
class Meta:
permissions = [('manage_payment_access', 'Manage payment access')]
class PaymentApprovalPolicy(TimeStampedModel):
name = models.CharField('عنوان سیاست', max_length=100)
unit = models.ForeignKey('staff.OrganizationalUnit', null=True, blank=True, on_delete=models.PROTECT, verbose_name='واحد (خالی: عمومی)')
reviewers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='payment_review_policies', verbose_name='افراد الزامی تأیید')
seniors = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='payment_senior_policies', blank=True, verbose_name='تأییدکنندگان ارشد')
is_active = models.BooleanField(default=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['unit'], condition=models.Q(is_active=True, unit__isnull=False), name='pay_active_unit_policy'),
models.UniqueConstraint(fields=['is_active'], condition=models.Q(is_active=True, unit__isnull=True), name='pay_active_global_policy'),
]
permissions = [('manage_payment_approval_policy', 'Manage payment approval policy')]
def __str__(self):
return self.name
class PaymentNotice(PublicIdModel, TimeStampedModel):
issuance_key = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='payment_notices')
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='issued_payment_notices')
owning_unit = models.ForeignKey('staff.OrganizationalUnit', null=True, on_delete=models.PROTECT)
account = models.ForeignKey(CompanyBankAccount, on_delete=models.PROTECT)
account_snapshot = models.JSONField()
subject = models.CharField(max_length=200)
amount = models.BigIntegerField()
deposit_id = models.CharField(max_length=100, blank=True)
invoice_number = models.CharField(max_length=100, blank=True)
description = models.TextField(blank=True)
cancelled_at = models.DateTimeField(null=True, blank=True)
deleted_at = models.DateTimeField(null=True, blank=True, editable=False)
class Meta:
ordering = ['-created_at', '-pk']
constraints = [models.CheckConstraint(condition=models.Q(amount__gt=0, amount__lte=MAX_AMOUNT), name='pay_notice_amount')]
indexes = [models.Index(fields=['customer', '-created_at']), models.Index(fields=['owning_unit', '-created_at'])]
@property
def number(self):
return f'PN-{self.public_id.hex[:12].upper()}'
class PaymentReceipt(PublicIdModel, TimeStampedModel):
class Status(models.TextChoices):
PENDING = 'PENDING', 'در انتظار بررسی'
PARTIAL = 'PARTIAL', 'تأیید ناقص'
APPROVED = 'APPROVED', 'تأیید شده'
REJECTED = 'REJECTED', 'رد شده'
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='payment_receipts')
submitted_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='submitted_payment_receipts')
notice = models.ForeignKey(PaymentNotice, null=True, blank=True, on_delete=models.PROTECT, related_name='receipts')
owning_unit = models.ForeignKey('staff.OrganizationalUnit', null=True, on_delete=models.PROTECT)
status = models.CharField(max_length=12, choices=Status.choices, default=Status.PENDING, db_index=True)
submission_key = models.UUIDField(unique=True)
current_version = models.PositiveIntegerField(default=1)
class Meta:
ordering = ['-created_at', '-pk']
indexes = [models.Index(fields=['customer', '-created_at']), models.Index(fields=['status', '-created_at'])]
permissions = [('approve_paymentreceipt', 'Approve payment receipt'), ('senior_approve_paymentreceipt', 'Senior final approval'), ('submit_customer_receipt', 'Submit receipt for customer'), ('view_all_payments', 'View payments across units')]
@property
def number(self):
return f'PR-{self.public_id.hex[:12].upper()}'
def receipt_path(instance, filename):
return f'payments/{instance.created_at:%Y/%m}/{uuid.uuid4().hex}{Path(filename).suffix.lower()}'
class PaymentReceiptRevision(models.Model):
receipt = models.ForeignKey(PaymentReceipt, on_delete=models.PROTECT, related_name='revisions')
version = models.PositiveIntegerField()
amount = models.BigIntegerField()
paid_date = models.DateField()
destination = models.ForeignKey(CompanyBankAccount, on_delete=models.PROTECT)
account_snapshot = models.JSONField()
origin_bank = models.ForeignKey(BankInstitution, null=True, blank=True, on_delete=models.PROTECT)
origin_other = models.CharField(max_length=100, blank=True)
deposit_id = models.CharField(max_length=100, blank=True)
tracking_number = models.CharField(max_length=100, blank=True)
description = models.TextField(blank=True)
destination_mismatch = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
image = models.FileField(upload_to=receipt_path, max_length=500)
mime_type = models.CharField(max_length=100)
sha256 = models.CharField(max_length=64)
original_name = models.CharField(max_length=255)
file_size = models.PositiveBigIntegerField()
class Meta:
constraints = [models.UniqueConstraint(fields=['receipt', 'version'], name='pay_receipt_revision'), models.CheckConstraint(condition=models.Q(amount__gt=0, amount__lte=MAX_AMOUNT), name='pay_receipt_amount')]
class ReceiptApprovalCycle(models.Model):
revision = models.OneToOneField(PaymentReceiptRevision, on_delete=models.PROTECT, related_name='cycle')
policy_snapshot = models.JSONField(default=dict)
status = models.CharField(max_length=12, choices=PaymentReceipt.Status.choices, default=PaymentReceipt.Status.PENDING)
finalized_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.PROTECT)
finalized_at = models.DateTimeField(null=True, blank=True)
class ReceiptApprovalAssignment(models.Model):
cycle = models.ForeignKey(ReceiptApprovalCycle, on_delete=models.PROTECT, related_name='assignments')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
is_senior = models.BooleanField(default=False)
class Meta:
constraints = [models.UniqueConstraint(fields=['cycle', 'user', 'is_senior'], name='pay_cycle_assignment')]
class ReceiptDecision(models.Model):
cycle = models.ForeignKey(ReceiptApprovalCycle, on_delete=models.PROTECT, related_name='decisions')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
action = models.CharField(max_length=20, choices=[('APPROVE', 'تأیید'), ('REJECT', 'رد'), ('SENIOR_APPROVE', 'تأیید ارشد')])
reason = models.TextField(blank=True)
actor_name = models.CharField(max_length=150)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [models.UniqueConstraint(fields=['cycle', 'user'], name='pay_one_vote_per_user')]
class PaymentAuditEvent(models.Model):
actor = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
actor_name = models.CharField(max_length=150)
action = models.CharField(max_length=50)
object_type = models.CharField(max_length=50)
object_id = models.CharField(max_length=64)
details = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ['-created_at', '-pk']
permissions = [('view_payment_audit', 'View financial audit')]
class PaymentAccessLink(models.Model):
"""Revocable capability, separate from customer login and ticket magic links."""
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
notice = models.ForeignKey(PaymentNotice, null=True, blank=True, on_delete=models.PROTECT)
receipt = models.ForeignKey(PaymentReceipt, null=True, blank=True, on_delete=models.PROTECT)
token_hash = models.CharField(max_length=64, unique=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
revoked_at = models.DateTimeField(null=True, blank=True)
submitted_receipts = models.ManyToManyField(PaymentReceipt, blank=True, related_name='access_links')
view_count = models.PositiveIntegerField(default=0)
last_used_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
constraints = [models.CheckConstraint(
condition=models.Q(notice__isnull=False) | models.Q(receipt__isnull=False),
name='payment_link_has_target')]
verbose_name = 'لینک موقت پرداخت'
verbose_name_plural = 'لینک‌های موقت پرداخت'
class PaymentNotification(models.Model):
job = models.OneToOneField('notifications.NotificationJob', on_delete=models.PROTECT, related_name='payment_notification')
notice = models.ForeignKey(PaymentNotice, null=True, on_delete=models.PROTECT)
cycle = models.ForeignKey(ReceiptApprovalCycle, null=True, on_delete=models.PROTECT)
unit = models.ForeignKey('staff.OrganizationalUnit', null=True, on_delete=models.PROTECT)
context = models.JSONField()
rendered_body = models.TextField(blank=True)
template_snapshot = models.JSONField(default=dict)
access_link = models.ForeignKey(PaymentAccessLink, null=True, blank=True, on_delete=models.PROTECT)
class PaymentNotificationAttempt(models.Model):
notification = models.ForeignKey(PaymentNotification, on_delete=models.PROTECT, related_name='attempt_log')
started_at = models.DateTimeField(auto_now_add=True)
finished_at = models.DateTimeField(null=True)
success = models.BooleanField(null=True)
provider_code = models.CharField(max_length=50, blank=True)
provider_id = models.CharField(max_length=500, blank=True)
error = models.TextField(blank=True)