"""DP payment logic methods.""" from datetime import date from typing import Optional from collaborator.constants import error from collaborator.constants.transaction import ( DESCRIPTION_RETURNED_PAYMENT, TYPE_DIRECT_PAYMENT, TransactionType, ) from collaborator.models.rds.dp_payment import ( DpPayment, PayoneerEventType, PayoneerPaymentStatus, ) from collaborator.models.rds.dp_payment_persister import DpPaymentPersister from collaborator.models.rds.statement_period_persister import StatementPeriodPersister from collaborator.models.rds.transaction_persister import TransactionPersister from collaborator.schemas.dp_payment import DpPaymentSchema from collaborator.utils import logging from collaborator.utils.error import OwsError from collaborator.utils.typing import User def get_by_payoneer_payment_id(client_reference_id: str) -> Optional[DpPayment]: """Fetch a DpPayment by its Payoneer payment ID.""" return DpPaymentPersister.get_by_payoneer_payment_id(client_reference_id) def update_payoneer_status( dp_payment_id, event_type: PayoneerEventType, reason: Optional[str], user: User ) -> None: """Update the Payoneer status of a DpPayment.""" status = _get_status_by_event_type(event_type) DpPaymentPersister.update_payoneer_status( dp_payment_id, status, event_type, reason, user=user ) def get_dp_payments( abacus_statement_period_id: Optional[int] = None, collaborator_id: Optional[int] = None, account_id: Optional[int] = None, payoneer_program_id: Optional[str] = None, payoneer_status: Optional[str] = None, sort_key: Optional[str] = None, sort_direction: Optional[str] = None, ) -> tuple[list[DpPaymentSchema], float]: """Logic for fetching DP payments. Args: abacus_statement_period_id: When provided only payments belonging to that statement period are returned. When `None` payments across all statement periods are returned. collaborator_id: When provided only payments belonging to that collaborator are returned. account_id: When provided only payments belonging to that account are returned. payoneer_program_id: When provided only payments with that Payoneer program ID are returned. payoneer_status: When provided only payments with that Payoneer status are returned. Returns: A tuple of (list of DpPaymentSchema, total_amount). """ rows = DpPaymentPersister.get_by_filters( abacus_statement_period_id=abacus_statement_period_id, collaborator_id=collaborator_id, account_id=account_id, payoneer_program_id=payoneer_program_id, payoneer_status=payoneer_status, sort_key=sort_key, sort_direction=sort_direction, ) total_amount = list(rows[0])[1] if len(rows) else 0.0 return [DpPaymentSchema.parse(payment) for payment, _ in rows], total_amount def handle_payment_event( payment: DpPayment, user: User, event_type: PayoneerEventType ) -> None: """Handle a payment event based on the event type.""" match event_type: case PayoneerEventType.PAYMENT_ACCEPTED: _handle_payment_accepted(payment, user) case PayoneerEventType.PAYMENT_CANCELLED: _handle_payment_returned(payment) case PayoneerEventType.PAYMENT_COMPLETED | PayoneerEventType.IACH_FAILED: # no transaction action needed for completed or failed payments yet, # just update the payment status in our database. pass def _handle_payment_accepted(dp_payment: DpPayment, user: User) -> None: """Create a DIRECT_PAYMENT transaction when a payment_accepted webhook is received. Idempotent: no-op if the dp_payment already has a collaborator_transaction_id. Args: dp_payment: The DpPayment instance from the webhook lookup. """ if dp_payment.collaborator_transaction_id is not None: return statement_period = StatementPeriodPersister.get_open_statement_period( dp_payment.account_id ) if statement_period is None: raise OwsError.not_found( code=error.ERROR_CODE_STATEMENT_PERIOD_NOT_FOUND, message=error.ERROR_MESSAGE_STATEMENT_PERIOD_NOT_FOUND, ) DpPaymentPersister.create_and_link_transaction( dp_payment_id=dp_payment.dp_payment_id, collaborator_id=dp_payment.collaborator_id, transaction_type=TYPE_DIRECT_PAYMENT, transaction_date=date.today(), description=f"{dp_payment.abacus_statement_period_name} Balance Payment", original_amount=-dp_payment.amount, chargeable_amount=-dp_payment.amount, statement_period_id=statement_period.statement_period_id, currency=dp_payment.currency, user=user, ) def _handle_payment_returned(dp_payment: DpPayment) -> None: """Create a TYPE_CREDIT transaction when a payment_cancelled event is received. Idempotent: no-op if a credit transaction already exists for the original payment. Also a no-op if no direct payment transaction was ever created (collaborator_transaction_id is None). Args: dp_payment: The DpPayment instance from the webhook lookup. """ if dp_payment.collaborator_transaction_id is None: return if TransactionPersister.is_credited_by_credited_payment_id( dp_payment.collaborator_transaction_id ): return original_transaction = TransactionPersister.get_by_id( dp_payment.collaborator_transaction_id ) description = DESCRIPTION_RETURNED_PAYMENT.format( desc=original_transaction["description"] ) open_statement_period = StatementPeriodPersister.get_open_statement_period( dp_payment.account_id ) if open_statement_period is None: raise OwsError.not_found( code=error.ERROR_CODE_STATEMENT_PERIOD_NOT_FOUND, message=error.ERROR_MESSAGE_STATEMENT_PERIOD_NOT_FOUND, ) created = TransactionPersister.create_transaction( collaborator_id=dp_payment.collaborator_id, transaction_type=TransactionType.CREDIT, transaction_date=date.today(), description=description, original_amount=-original_transaction["chargeable_amount"], collaborator_share=None, chargeable_amount=-original_transaction["chargeable_amount"], statement_period_id=open_statement_period.statement_period_id, transferwise_transaction_id=None, report_id=None, voided_transaction_id=None, currency=dp_payment.currency, credited_payment_id=dp_payment.collaborator_transaction_id, ) logging.log_event( logging.LOG_EVENT_CREATE, "transaction", created["id"], None, created, None, ) def _get_status_by_event_type( event: PayoneerEventType, ) -> PayoneerPaymentStatus: """Map a PayoneerEventType to the corresponding PayoneerPaymentStatus name.""" match event: case PayoneerEventType.PAYMENT_ACCEPTED: return PayoneerPaymentStatus.RUNNING case PayoneerEventType.PAYMENT_COMPLETED: return PayoneerPaymentStatus.COMPLETE case PayoneerEventType.PAYMENT_CANCELLED: return PayoneerPaymentStatus.REJECTED case PayoneerEventType.IACH_FAILED: return PayoneerPaymentStatus.ERROR