Example #1
0
    def test_file_has_initiating_party_in_group_header(self, app):
        # Given
        offerer = create_offerer()
        user = create_user()
        venue = create_venue(offerer)
        stock = create_stock_from_offer(create_offer_with_thing_product(venue))
        booking = create_booking(user=user, stock=stock)
        payment1 = create_payment(booking, offerer, Decimal(10), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payment2 = create_payment(booking, offerer, Decimal(20), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payments = [payment1, payment2]

        # When
        xml = generate_message_file(payments, "BD12AZERTY123456", "AZERTY9Q666", MESSAGE_ID, "0000")

        # Then
        assert (
            find_node("//ns:GrpHdr/ns:InitgPty/ns:Nm", xml) == "pass Culture"
        ), 'The initiating party should be "pass Culture"'
Example #2
0
    def test_file_has_ultimate_debtor_in_transaction_info(self, app):
        # Given
        offerer = create_offerer()
        user = create_user()
        venue = create_venue(offerer)
        stock = create_stock_from_offer(create_offer_with_thing_product(venue))
        booking = create_booking(user=user, stock=stock)
        payment1 = create_payment(booking, offerer, Decimal(10), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payment2 = create_payment(booking, offerer, Decimal(20), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payments = [payment1, payment2]

        # When
        xml = generate_message_file(payments, "BD12AZERTY123456", "AZERTY9Q666", MESSAGE_ID, "0000")

        # Then
        assert (
            find_node("//ns:PmtInf/ns:CdtTrfTxInf/ns:UltmtDbtr/ns:Nm", xml) == "pass Culture"
        ), 'The ultimate debtor name should be "pass Culture"'
Example #3
0
    def test_file_has_creation_datetime_in_group_header(self, app):
        # Given
        offerer = create_offerer()
        user = create_user()
        venue = create_venue(offerer)
        stock = create_stock_from_offer(create_offer_with_thing_product(venue))
        booking = create_booking(user=user, stock=stock)
        payment1 = create_payment(booking, offerer, Decimal(10), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payment2 = create_payment(booking, offerer, Decimal(20), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payments = [payment1, payment2]

        # When
        xml = generate_message_file(payments, "BD12AZERTY123456", "AZERTY9Q666", MESSAGE_ID, "0000")

        # Then
        assert (
            find_node("//ns:GrpHdr/ns:CreDtTm", xml) == "2018-10-15T09:21:34"
        ), 'The creation datetime should look like YYYY-MM-DDTHH:MM:SS"'
Example #4
0
    def test_file_has_custom_message_id_in_group_header(self, app):
        # Given
        offerer = create_offerer()
        user = create_user()
        venue = create_venue(offerer)
        stock = create_stock_from_offer(create_offer_with_thing_product(venue))
        booking = create_booking(user=user, stock=stock)
        payment1 = create_payment(booking, offerer, Decimal(10), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payment2 = create_payment(booking, offerer, Decimal(20), iban="CF13QSDFGH456789", bic="QSDFGH8Z555")
        payments = [payment1, payment2]

        # When
        xml = generate_message_file(payments, "BD12AZERTY123456", "AZERTY9Q666", MESSAGE_ID, "0000")

        # Then
        assert (
            find_node("//ns:GrpHdr/ns:MsgId", xml) == MESSAGE_ID
        ), 'The message id should look like "passCulture-SCT-YYYYMMDD-HHMMSS"'
    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)