def send_transactions( payments: List[Payment], pass_culture_iban: Optional[str], pass_culture_bic: Optional[str], pass_culture_remittance_code: Optional[str], recipients: List[str], ) -> None: if not pass_culture_iban or not pass_culture_bic or not pass_culture_remittance_code: raise Exception( "[BATCH][PAYMENTS] Missing PASS_CULTURE_IBAN[%s], PASS_CULTURE_BIC[%s] or " "PASS_CULTURE_REMITTANCE_CODE[%s] in environment variables" % (pass_culture_iban, pass_culture_bic, pass_culture_remittance_code)) message_name = "passCulture-SCT-%s" % datetime.strftime( datetime.utcnow(), "%Y%m%d-%H%M%S") xml_file = generate_message_file(payments, pass_culture_iban, pass_culture_bic, message_name, pass_culture_remittance_code) logger.info("[BATCH][PAYMENTS] Payment message name : %s", message_name) try: validate_message_file_structure(xml_file) except DocumentInvalid as exception: for payment in payments: payment.setStatus(TransactionStatus.NOT_PROCESSABLE, detail=str(exception)) repository.save(*payments) raise checksum = generate_file_checksum(xml_file) message = generate_payment_message(message_name, checksum, payments) logger.info( "[BATCH][PAYMENTS] Sending file with message ID [%s] and checksum [%s]", message.name, message.checksum.hex()) logger.info("[BATCH][PAYMENTS] Recipients of email : %s", recipients) successfully_sent_payments = send_payment_message_email( xml_file, checksum, recipients) logger.info("[BATCH][PAYMENTS] Updating status of %d payments", len(payments)) if successfully_sent_payments: for payment in payments: payment.setStatus(TransactionStatus.SENT) else: for payment in payments: payment.setStatus(TransactionStatus.ERROR, detail="Erreur d'envoi à MailJet") repository.save(message, *payments)
def test_validate_message_file_structure_raises_on_error(app): # given transaction_file = """ <broken><xml></xml></broken> """ # when with pytest.raises(DocumentInvalid) as e: validate_message_file_structure(transaction_file) # then assert str( e.value ) == "Element 'broken': No matching global declaration available for the validation root., line 2"
def test_basics(self): iban1, bic1 = "CF13QSDFGH456789", "QSDFGH8Z555" batch_date = datetime.datetime.now() offerer1 = offers_factories.OffererFactory(siren="siren1") p1 = payments_factories.PaymentFactory( batchDate=batch_date, amount=10, iban=iban1, bic=bic1, transactionLabel="remboursement 1ère quinzaine 09-2018", recipientName="first offerer", booking__stock__offer__venue__managingOfferer=offerer1, ) offerer2 = offers_factories.OffererFactory(siren="siren2") iban2, bic2 = "FR14WXCVBN123456", "WXCVBN7B444" p2 = payments_factories.PaymentFactory( batchDate=batch_date, amount=20, iban=iban2, bic=bic2, recipientName="second offerer", transactionLabel="remboursement 1ère quinzaine 09-2018", booking__stock__offer__venue__managingOfferer=offerer2, ) payments_factories.PaymentFactory( batchDate=batch_date, amount=40, iban=iban2, bic=bic2, recipientName="second offerer", transactionLabel="remboursement 1ère quinzaine 09-2018", booking__stock__offer__venue__managingOfferer=offerer2, ) recipient_iban = "BD12AZERTY123456" recipient_bic = "AZERTY9Q666" message_id = "passCulture-SCT-20181015-114356" remittance_code = "remittance-code" with freeze_time("2018-10-15 09:21:34"): xml = generate_message_file(Payment.query, batch_date, recipient_iban, recipient_bic, message_id, remittance_code) # Group header assert ( find_node("//ns:GrpHdr/ns:MsgId", xml) == message_id ), 'The message id should look like "passCulture-SCT-YYYYMMDD-HHMMSS"' assert ( find_node("//ns:GrpHdr/ns:CreDtTm", xml) == "2018-10-15T09:21:34" ), 'The creation datetime should look like YYYY-MM-DDTHH:MM:SS"' assert (find_node("//ns:GrpHdr/ns:InitgPty/ns:Nm", xml) == "pass Culture" ), 'The initiating party should be "pass Culture"' assert ( find_node("//ns:GrpHdr/ns:CtrlSum", xml) == "70.00" ), "The control sum should match the total amount of money to pay" assert ( find_node("//ns:GrpHdr/ns:NbOfTxs", xml) == "2" ), "The number of transactions should match the distinct number of IBANs" # Payment info assert ( find_node("//ns:PmtInf/ns:PmtInfId", xml) == message_id ), "The payment info id should be the same as message id since we only send one payment per XML message" assert ( find_node("//ns:PmtInf/ns:NbOfTxs", xml) == "2" ), "The number of transactions should match the distinct number of IBANs" assert ( find_node("//ns:PmtInf/ns:CtrlSum", xml) == "70.00" ), "The control sum should match the total amount of money to pay" assert ( find_node("//ns:PmtInf/ns:PmtMtd", xml) == "TRF" ), "The payment method should be TRF because we want to transfer money" assert find_node("//ns:PmtInf/ns:PmtTpInf/ns:SvcLvl/ns:Cd", xml) == "SEPA" assert ( find_node("//ns:PmtInf/ns:PmtTpInf/ns:CtgyPurp/ns:Cd", xml) == "GOVT" ), "The category purpose should be GOVT since we handle government subventions" assert find_node("//ns:PmtInf/ns:DbtrAgt/ns:FinInstnId/ns:BIC", xml) == recipient_bic assert find_node("//ns:PmtInf/ns:DbtrAcct/ns:Id/ns:IBAN", xml) == recipient_iban assert (find_node("//ns:PmtInf/ns:Dbtr/ns:Nm", xml) == "pass Culture" ), 'The name of the debtor should be "pass Culture"' assert ( find_node("//ns:PmtInf/ns:ReqdExctnDt", xml) == "2018-10-22" ), "The requested execution datetime should be in one week from now" assert ( find_node("//ns:PmtInf/ns:ChrgBr", xml) == "SLEV" ), 'The charge bearer should be SLEV as in "following service level"' assert (find_node("//ns:PmtInf/ns:CdtTrfTxInf/ns:UltmtDbtr/ns:Nm", xml) == "pass Culture" ), 'The ultimate debtor name should be "pass Culture"' assert find_node("//ns:InitgPty/ns:Id/ns:OrgId/ns:Othr/ns:Id", xml) == remittance_code # Transaction-specific content ibans = find_all_nodes( "//ns:PmtInf/ns:CdtTrfTxInf/ns:CdtrAcct/ns:Id/ns:IBAN", xml) assert ibans == [iban1, iban2] bics = find_all_nodes( "//ns:PmtInf/ns:CdtTrfTxInf/ns:CdtrAgt/ns:FinInstnId/ns:BIC", xml) assert bics == [bic1, bic2] names = find_all_nodes("//ns:PmtInf/ns:CdtTrfTxInf/ns:Cdtr/ns:Nm", xml) assert names == ["first offerer", "second offerer"] sirens = find_all_nodes( "//ns:PmtInf/ns:CdtTrfTxInf/ns:Cdtr/ns:Id/ns:OrgId/ns:Othr/ns:Id", xml) assert sirens == ["siren1", "siren2"] labels = find_all_nodes( "//ns:PmtInf/ns:CdtTrfTxInf/ns:RmtInf/ns:Ustrd", xml) assert labels == list(("remboursement 1ère quinzaine 09-2018", ) * 2) amounts = find_all_nodes( "//ns:PmtInf/ns:CdtTrfTxInf/ns:Amt/ns:InstdAmt", xml) assert amounts == ["10.00", "60.00"] e2e_ids = find_all_nodes( "//ns:PmtInf/ns:CdtTrfTxInf/ns:PmtId/ns:EndToEndId", xml) assert e2e_ids == [ p1.transactionEndToEndId.hex, p2.transactionEndToEndId.hex ] # Finally, make sure that the file is valid validate_message_file_structure(xml)
def send_transactions( payment_query, batch_date: datetime, pass_culture_iban: Optional[str], pass_culture_bic: Optional[str], pass_culture_remittance_code: Optional[str], recipients: list[str], ) -> None: if not pass_culture_iban or not pass_culture_bic or not pass_culture_remittance_code: raise Exception( "[BATCH][PAYMENTS] Missing PASS_CULTURE_IBAN[%s], PASS_CULTURE_BIC[%s] or " "PASS_CULTURE_REMITTANCE_CODE[%s] in environment variables" % (pass_culture_iban, pass_culture_bic, pass_culture_remittance_code)) logger.info("[BATCH][PAYMENTS] Generating venues file") venues_csv = generate_venues_csv(payment_query) logger.info("[BATCH][PAYMENTS] Generating XML file") message_name = "passCulture-SCT-%s" % datetime.strftime( datetime.utcnow(), "%Y%m%d-%H%M%S") xml_file = generate_message_file(payment_query, batch_date, pass_culture_iban, pass_culture_bic, message_name, pass_culture_remittance_code) logger.info("[BATCH][PAYMENTS] Payment message name : %s", message_name) # The following may raise a DocumentInvalid exception. This is # usually because the data is incorrect. In that case, let the # exception bubble up and stop the calling function so that we can # fix the data and run the function again. validate_message_file_structure(xml_file) checksum = hashlib.sha256(xml_file.encode("utf-8")).digest() message = PaymentMessage(name=message_name, checksum=checksum) db.session.add(message) db.session.commit() # We cannot directly call "update()" when "join()" has been called. # fmt: off (db.session.query(Payment).filter( Payment.id.in_(payment_query.with_entities(Payment.id))).update( {"paymentMessageId": message.id}, synchronize_session=False)) # fmt: on db.session.commit() logger.info( "[BATCH][PAYMENTS] Sending file with message ID [%s] and checksum [%s]", message.name, message.checksum.hex()) logger.info("[BATCH][PAYMENTS] Recipients of email: %s", recipients) venues_csv_path = _save_file_on_disk("venues", venues_csv, "csv") xml_path = _save_file_on_disk("banque_de_france", xml_file, "xml") if not send_payment_message_email(xml_file, venues_csv, checksum, recipients): logger.info( "[BATCH][PAYMENTS] Could not send payment message email. Files have been stored at %s and %s", venues_csv_path, xml_path, ) logger.info( "[BATCH][PAYMENTS] Updating status of payments to UNDER_REVIEW") payments_api.bulk_create_payment_statuses(payment_query, TransactionStatus.UNDER_REVIEW, detail=None)