add payment_link and banks
This commit is contained in:
1 parent
2ae79b1ca4
commit
de3135b588
100 files changed
+6167
-47
No files matched your search
@@ -0,0 +1,234 @@
|
||||
"""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)
|
||||
|
||||
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)
|
||||
Reference in new issue
Block a user