def test_transfer_to_global_platform_owner_if_local_does_not_have_mangopay_account( mangopay_bank_alias_payin, api_client, order, platform_user, mailoutbox, admin, ): platform_user.mangopay_user_id = None platform_user.save() api_client.get( reverse("webhook"), data={"RessourceId": "payin-1", "EventType": "PAYIN_NORMAL_SUCCEEDED"}, ) transfer = mangopay_bank_alias_payin.return_value.transfer transfer.assert_any_call( "mangopay-buyer-1", "buyer-wallet-1", "seller-1-wallet", amount=178.98, fees=0, tag=f"v2 Order: {order.id} Document: Producer Invoice Seller: Best apples Buyer: ACME", ) transfer.assert_any_call( "mangopay-buyer-1", "buyer-wallet-1", "logistics-1-wallet", amount=17.03, fees=0, tag=f"v2 Order: {order.id} Document: Logistics Invoice Seller: Traidoo Buyer: ACME", ) transfer.assert_any_call( "mangopay-buyer-1", "buyer-wallet-1", "global-platform-1-wallet", amount=9.71, fees=0, tag=f"v2 Order: {order.id} Document: Credit Note Seller: Traidoo Buyer: Traidoo", ) transfer.assert_any_call( "mangopay-buyer-1", "buyer-wallet-1", "global-platform-1-wallet", amount=6.47, fees=1.08, tag=f"v2 Order: {order.id} Document: Platform Invoice Seller: Traidoo Buyer: Best apples", ) assert mailoutbox[2].subject == "Fehler bei der Verarbeitung der Zahlung" assert ( f"Cannot pay local platform owner his share 9.71 because " f"local platform owner `{platform_user.id}` does not have mangopay account. " f"Funds will be transfered to global platform owner" ) in mailoutbox[2].body assert set(mailoutbox[2].to) == set(get_admin_emails())
def handle_failed_payin(self): payin = self.mangopay.get_pay_in(self.resource_id) if payin["Status"] == "FAILED": body = ( f"Payin {self.resource_id} failed. Error message: " f"{payin['ResultMessage']}. Check mangopay for further details." ) logger.debug(f"handle_failed_payin: {body}") send_mail( region=self.get_region(), recipient_list=get_admin_emails(), subject="Fehler bei der Verarbeitung der Zahlung", template="mails/generic.html", context={"body": body}, )
def test_paid_too_much_bankwire_pay_in_to_wallet( mangopay_wire_reference_payin, order, order_items, mailoutbox, order_confirmation, logistics_invoice, producer_invoice, platform_invoice, api_client, admin, ): # Only applies to bankwire pay in to wallet # TODO Remove once all pay ins are migrated to banking alias pay ins mangopay_wire_reference_payin.return_value.get_pay_in.return_value = { "Status": "SUCCEEDED", "CreditedWalletId": "buyer-wallet-1", "CreditedFunds": {"Currency": "EUR"}, "DebitedFunds": {"Currency": "EUR", "Amount": 20033}, "Fees": {"Currency": "EUR", "Amount": 593}, "Id": "payin-1", "AuthorId": "mangopay-user-buyer", } api_client.get( reverse("webhook"), data={"RessourceId": "payin-1", "EventType": "PAYIN_NORMAL_SUCCEEDED"}, ) order.refresh_from_db() assert order.is_paid order_confirmation.refresh_from_db() assert order_confirmation.paid assert mailoutbox[0].subject == "Client paid too much" assert ( f"Too much received in payin payin-1 for document " f"{order_confirmation.id}. Expected (in cents) `18088`, but received " f"`20033`\n" ) in mailoutbox[0].body assert set(mailoutbox[0].to) == set(get_admin_emails())
def pay_for_invoice_from_pay_in(self, pay_in: Dict, invoice: Document): seller_id = invoice.seller["user_id"] buyer_id = invoice.buyer["user_id"] seller_profile = User.objects.get(pk=seller_id) if not seller_profile.mangopay_user_id: error_message = ( f"Cannot process payin `{self.resource_id}`. Seller " f"`{seller_id}` does not have mangopay account. " f"Please create an account and contact support to trigger order processing again" ) send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", template="mails/generic.html", context={"body": error_message}, recipient_list=get_admin_emails(), ) return seller_mangopay_wallet = self.get_user_wallet( seller_profile.mangopay_user_id) buyer_profile = User.objects.get(pk=buyer_id) buyer_mangopay_wallet_id = pay_in["CreditedWalletId"] if invoice.document_type == invoice.TYPES.get_value( "producer_invoice"): # Producer gets paid for invoice minus gross value of platform # invoice for his sale platform_invoice_for_this_producer = Document.objects.get( order_id=invoice.order.id, document_type=Document.TYPES.get_value("platform_invoice"), buyer__user_id=seller_id, ) amount = Decimal(str(invoice.price_gross)) - Decimal( str(platform_invoice_for_this_producer.price_gross)) else: amount = Decimal(str(invoice.price_gross)) amount = amount.quantize(Decimal(".01"), "ROUND_HALF_UP") amount = float(amount) try: pay_for_document( document=invoice, author_id=buyer_profile.mangopay_user_id, source_wallet_id=buyer_mangopay_wallet_id, destination_wallet_id=seller_mangopay_wallet["Id"], amount=amount, fees=0, ) except MangopayTransferError as mangopay_error: error_message = ( f"Transfer from buyer wallet {buyer_mangopay_wallet_id} to " f"seller wallet {seller_mangopay_wallet['Id']} failed. " f"Mangopay error: {mangopay_error}. " "Please contact support to investigate issue and trigger processing again." f"Affected document {invoice.id}") send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", template="mails/generic.html", context={"body": error_message}, recipient_list=get_admin_emails(), ) return except DuplicateTransferError as error: logger.exception(error) return send_mail( region=self.get_region(), subject=f"Zahlung erhalten für Auftrag #{invoice.order_id}", template="mails/payments/successful_payin.html", context={ "currency": CURRENT_CURRENCY_CODE, "amount": f"{amount:.2f}", "buyer_company_name": invoice.buyer["company_name"], "document_name": invoice.__class__.__name__, "order_number": invoice.order_id, }, recipient_list=[invoice.seller["email"]], ) self.send_task( f"/mangopay/cron/payouts/{seller_profile.mangopay_user_id}", queue_name="mangopay-payouts", http_method="POST", payload={ "order_id": invoice.order_id, "amount": amount }, headers={ "Region": invoice.order.region.slug, "Content-Type": "application/json", }, )
def pay_for_platform( self, pay_in: Dict, order_id: int, buyer_mangopay_user_id: str, total_order_value: float, global_platform_user_wallet: dict, global_platform_user: User, fees_charged_at_payin=False, ): platform_invoices = Document.objects.select_for_update( nowait=True).filter( order_id=order_id, paid=False, document_type__in=[ Document.TYPES.get_value("platform_invoice"), Document.TYPES.get_value("buyer_platform_invoice"), ], ) invoice = platform_invoices.first() if not invoice: return local_platform_fee_due = calculate_local_platform_fee_for_order( order_id, global_platform_user.id) buyer_mangopay_wallet_id = pay_in["CreditedWalletId"] global_platform_user_mangopay_id = global_platform_user_wallet[ "Owners"][0] total_unpaid_platform_invoices_value = calculate_platform_fee_for_order( order_id) mangopay_fees = self.mangopay_fees(total_order_value) mangopay_fees = Decimal(str(mangopay_fees)) amount_to_transfer_to_global_platform_owner = ( total_unpaid_platform_invoices_value - local_platform_fee_due) amount_to_payout_from_global_platform_owner_wallet = ( amount_to_transfer_to_global_platform_owner - mangopay_fees) if fees_charged_at_payin: amount_to_transfer_to_global_platform_owner -= mangopay_fees amount_to_transfer_to_global_platform_owner = ( amount_to_transfer_to_global_platform_owner.quantize( Decimal(".01"), "ROUND_HALF_UP")) amount_to_transfer_to_global_platform_owner = float( amount_to_transfer_to_global_platform_owner) try: # not using `pay_for_document` to avoid nested transactions self.mangopay.transfer( buyer_mangopay_user_id, buyer_mangopay_wallet_id, global_platform_user_wallet["Id"], amount=amount_to_transfer_to_global_platform_owner, fees=float(mangopay_fees) if not fees_charged_at_payin else 0, tag=invoice.mangopay_tag, ) except MangopayTransferError as transfer_error: error_message = ( f"Transfer from buyer wallet {buyer_mangopay_wallet_id} to " f"platform wallet {global_platform_user_wallet['Id']} failed. " f"Mangopay error: {transfer_error}. ") send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", template="mails/generic.html", context={"body": error_message}, recipient_list=get_admin_emails(), ) return for invoice in platform_invoices: invoice.paid = True invoice.save(update_fields=["paid", "updated_at"]) send_mail( region=self.get_region(), subject=f"Zahlung erhalten für Auftrag #{invoice.order_id}", template="mails/payments/successful_payin.html", context={ "currency": CURRENT_CURRENCY_CODE, "amount": f"{invoice.price_gross:.2f}", "buyer_company_name": invoice.buyer["company_name"], "document_name": invoice.__class__.__name__, "order_number": invoice.order_id, }, recipient_list=[invoice.seller["email"]], ) self.send_task( f"/mangopay/cron/payouts/{global_platform_user_mangopay_id}", queue_name="mangopay-payouts", http_method="POST", payload={ "order_id": invoice.order_id, "amount": float(amount_to_payout_from_global_platform_owner_wallet), }, headers={ "Region": invoice.order.region.slug, "Content-Type": "application/json", }, )
def pay_local_platform_owner( self, order_id: int, buyer_mangopay_user_id: str, buyer_mangopay_wallet_id: str, global_platform_user_wallet: dict, global_platform_user_id: int, ) -> Decimal: """ Tries to pay local platform owner :param global_platform_user_id: :param global_platform_user_wallet: :param buyer_mangopay_wallet_id: :param buyer_mangopay_user_id: :param order_id: :return: float value, amount paid for local platform owner """ try: credit_note_for_local_platform_owner = Document.objects.get( order_id=order_id, document_type=Document.TYPES.get_value("credit_note"), paid=False, seller__user_id=global_platform_user_id, ) except Document.DoesNotExist: logger.info( f"No credit notes issued by global platform owner f{global_platform_user_id}" ) return Decimal("0") local_platform_owner = User.objects.get( id=credit_note_for_local_platform_owner.buyer["user_id"]) if not local_platform_owner.mangopay_user_id: error_message = ( f"Cannot pay local platform owner his share {credit_note_for_local_platform_owner.price_gross} " f"because local platform owner `{local_platform_owner.id}` does not have mangopay account. " f"Funds will be transfered to global platform owner") send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", recipient_list=get_admin_emails(), template="mails/generic.html", context={"body": error_message}, ) # we are not using _pay_for_document() since we do not want to mark document as paid self.mangopay.transfer( buyer_mangopay_user_id, buyer_mangopay_wallet_id, global_platform_user_wallet["Id"], amount=credit_note_for_local_platform_owner.price_gross, fees=0, tag=credit_note_for_local_platform_owner.mangopay_tag, ) return Decimal( str(credit_note_for_local_platform_owner.price_gross)) local_platform_owner_wallet = self.get_user_wallet( local_platform_owner.mangopay_user_id) try: pay_for_document( document=credit_note_for_local_platform_owner, author_id=buyer_mangopay_user_id, source_wallet_id=buyer_mangopay_wallet_id, destination_wallet_id=local_platform_owner_wallet["Id"], amount=credit_note_for_local_platform_owner.price_gross, ) except MangopayTransferError as mangopay_error: error_message = (f"Cannot pay local platform owner." f"Mangopay transfer error {mangopay_error}") send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", template="mails/generic.html", recipient_list=get_admin_emails(), context={"body": error_message}, ) return Decimal("0") except DuplicateTransferError: return Decimal("0") self.send_task( f"/mangopay/cron/payouts/{local_platform_owner.mangopay_user_id}", queue_name="mangopay-payouts", http_method="POST", payload={ "order_id": order_id, "amount": credit_note_for_local_platform_owner.price_gross, }, headers={ "Region": local_platform_owner.region.slug, "Content-Type": "application/json", }, ) return Decimal(str(credit_note_for_local_platform_owner.price_gross))
def handle_successful_pay_in(self): pay_in = self.mangopay.get_pay_in(self.resource_id) if pay_in["Status"] != "SUCCEEDED" and not self.skip_checks: send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", recipient_list=get_admin_emails(), template="mails/generic.html", context={ "body": (f"Successfully received {self.resource_id} hook, but " f"actual status is `{pay_in['Status']}`. Check with Mangopay details" ) }, ) return banking_alias_id = pay_in.get("BankingAliasId") if not banking_alias_id: # Old order return self._process_as_direct_pay_in(pay_in) banking_alias = self.mangopay.get_banking_alias(banking_alias_id) wallet_id = banking_alias["WalletId"] mangopay_user_id = banking_alias["CreditedUserId"] buyer = User.objects.get(mangopay_user_id=mangopay_user_id) order_confirmation_documents = ( Document.objects.select_related("order").filter( paid=False, document_type=Document.TYPES.get_value( "order_confirmation_buyer"), buyer__user_id=buyer.id, payment_reference=None, ).order_by("created_at")) for order_confirmation_document in order_confirmation_documents: wallet = self.mangopay.get_wallet(wallet_id) if not sufficient_wallet_balance_for_order( order_confirmation_document.order_id, buyer.id, wallet): logger.info( f"No more cash in wallet to pay for order {order_confirmation_document.order_id}" ) break order_invoices = ( Document.objects.select_related("order").filter( order_id=order_confirmation_document.order.id, paid=False, # Do not pay platform invoice here, since there's extra calculation document_type__in=[ Document.TYPES.get_value("logistics_invoice"), Document.TYPES.get_value("producer_invoice"), ], ).order_by("-document_type")) for invoice in order_invoices: logger.debug("Paying invoice {}".format(invoice.id)) self.pay_for_invoice_from_pay_in(pay_in, invoice) # Take the first platform invoice to get platform user information global_platform_user = get_platform_user_for_order( order_confirmation_document.order_id) if not global_platform_user.mangopay_user_id: error_message = ( f"Cannot process payin `{self.resource_id}` and pay for platform." f"Platform user `{global_platform_user.id}` does not have mangopay account. " f"Please create an account and process trigger payin processing again." ) send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", recipient_list=get_admin_emails(), template="mails/generic.html", context={"body": error_message}, ) return global_platform_user_wallet = self.get_user_wallet( global_platform_user.mangopay_user_id) self.pay_local_platform_owner( order_confirmation_document.order_id, order_confirmation_document.order.buyer.mangopay_user_id, pay_in["CreditedWalletId"], global_platform_user_wallet, global_platform_user.id, ) # Pay combined platform fee in one transfer from buyer wallet try: self.pay_for_platform( pay_in, order_confirmation_document.order.id, order_confirmation_document.order.buyer.mangopay_user_id, total_order_value=order_confirmation_document.price_gross, global_platform_user_wallet=global_platform_user_wallet, global_platform_user=global_platform_user, ) except OperationalError as error: logger.warning( f"Other process is trying to pay for platform: `{error}`") self.try_to_set_order_as_paid(order_confirmation_document.order) wallet = self.mangopay.get_wallet(wallet_id) if wallet["Balance"]["Amount"] > 0: error_message = ( f"After processing payin `{self.resource_id}` " f"buyer with mangopay id {mangopay_user_id} still has positive wallet" f"balance {wallet['Balance']['Amount']} cents") send_mail( region=self.get_region(), subject="User has extra cash in wallet", recipient_list=get_admin_emails(), template="mails/generic.html", context={"body": error_message}, ) return
def _process_as_direct_pay_in(self, pay_in: Dict): try: document = Document.objects.select_related("order").get( mangopay_payin_id=self.resource_id) except Document.DoesNotExist: send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", recipient_list=get_admin_emails(), template="mails/generic.html", context={ "body": f"Document related to payin {self.resource_id} not found. " }, ) return if document.paid: logger.warning( (f"Invoice `{document.id}` already paid. Probably " f"duplicated notification for payin {self.resource_id} " f"from mangopay. ")) return order = document.order pay_in_currency = pay_in["DebitedFunds"]["Currency"] if pay_in_currency != "EUR": raise PaymentError( (f"Wrong currency `{pay_in_currency}` for payin " f"{ self.resource_id}, expected EUR. ")) if document.document_type == document.TYPES.get_value( "order_confirmation_buyer"): pay_in_amount = pay_in["DebitedFunds"]["Amount"] if pay_in_amount < document.price_gross_cents and not self.skip_checks: logger.debug( f"Document gross cents: {document.price_gross_cents}") send_mail( region=self.get_region(), subject="Fehler bei der Verarbeitung der Zahlung", recipient_list=get_admin_emails(), template="mails/generic.html", context={ "body": (f"Wrong amount received in payin {self.resource_id} " f"for document {document.id}. Expected (in cents) " f"`{document.price_gross_cents}`, but received " f"`{pay_in_amount}`\n") }, ) return Response("Falied") elif pay_in_amount > document.price_gross_cents and not self.skip_checks: logger.debug( f"Document gross cents: {document.price_gross_cents}") send_mail( region=self.get_region(), subject="Client paid too much", recipient_list=get_admin_emails(), template="mails/generic.html", context={ "body": (f"Too much received in payin {self.resource_id} for " f"document {document.id}. Expected (in cents) " f"`{document.price_gross_cents}`, but received " f"`{pay_in_amount}`\n") }, ) # Get unpaid documents for the order order_invoices = Document.objects.filter( order_id=order.id, paid=False, # Do not process platform invoices document_type__in=[ Document.TYPES.get_value("logistics_invoice"), Document.TYPES.get_value("producer_invoice"), ], ).order_by("-document_type") for invoice in order_invoices: logger.debug("Paying invoice {}".format(invoice.id)) self.pay_for_invoice_from_pay_in(pay_in, invoice) global_platform_user = get_platform_user_for_order(order.id) global_platform_user_wallet = self.get_user_wallet( global_platform_user.mangopay_user_id) self.pay_local_platform_owner( order.id, order.buyer.mangopay_user_id, pay_in["CreditedWalletId"], global_platform_user_wallet, global_platform_user.id, ) # Pay combined platform fee in one transfer from buyer wallet try: self.pay_for_platform( pay_in, order.id, order.buyer.mangopay_user_id, order.total_price, global_platform_user_wallet, global_platform_user, fees_charged_at_payin=True, ) except OperationalError as error: logger.warning( f"Other process is trying to pay for platform: `{error}`") self.try_to_set_order_as_paid(order) else: raise PaymentError( "Received payment for unexpected document type {}, payin id: {}" .format(document.document_type, self.resource_id))