Exemple #1
0
def test_order_fulfilled(mocker):  # pylint:disable=too-many-arguments
    """
    Test the happy case
    """
    complete_order_mock = mocker.patch("b2b_ecommerce.api.complete_b2b_order")
    order = B2BOrderFactory.create(status=B2BOrder.CREATED)

    data = {}
    for _ in range(5):
        data[FAKE.text()] = FAKE.text()

    data["req_reference_number"] = order.reference_number
    data["decision"] = "ACCEPT"

    fulfill_b2b_order(data)

    order.refresh_from_db()
    assert order.status == B2BOrder.FULFILLED
    assert order.b2breceipt_set.count() == 1
    assert order.b2breceipt_set.first().data == data

    assert B2BOrderAudit.objects.count() == 1
    order_audit = B2BOrderAudit.objects.last()
    assert order_audit.order == order
    assert dict_without_keys(order_audit.data_after,
                             "updated_on") == dict_without_keys(
                                 order.to_dict(), "updated_on")
    complete_order_mock.assert_called_once_with(order)
Exemple #2
0
def test_order_status_customer_name(client):
    """
    Test that the correct customer name is returned by order status API
    """
    order = B2BOrderFactory.create()
    resp = client.get(
        reverse("b2b-order-status", kwargs={"hash": str(order.unique_id)}))

    # Order status API returns no customer name if there is no existing user associated with order email and there is no
    # receipt associated with order
    assert resp.status_code == status.HTTP_200_OK
    assert resp.json().get("customer_name") == ""
    receipt_date = {
        "req_bill_to_forename": "Test",
        "req_bill_to_surname": "Name"
    }

    # Order status API returns customer name from the receipt data when there is no user account with order email

    B2BReceipt.objects.create(order=order, data=receipt_date)
    resp = client.get(
        reverse("b2b-order-status", kwargs={"hash": str(order.unique_id)}))
    assert resp.status_code == status.HTTP_200_OK
    assert resp.json().get("customer_name") == "Test Name"

    # Order status API returns name of the existing user if there exists an order's email matching user account

    test_user = UserFactory.create(email=order.email)
    resp = client.get(
        reverse("b2b-order-status", kwargs={"hash": str(order.unique_id)}))
    assert resp.status_code == status.HTTP_200_OK
    assert resp.json().get("customer_name") == test_user.name
Exemple #3
0
def test_order_status(client):
    """
    The order status API should provide information about the order based on its unique_id.
    """
    order = B2BOrderFactory.create()
    resp = client.get(
        reverse("b2b-order-status", kwargs={"hash": str(order.unique_id)})
    )
    assert resp.status_code == status.HTTP_200_OK
    assert_drf_json_equal(
        resp.json(),
        {
            "email": order.email,
            "num_seats": order.num_seats,
            "product_version": FullProductVersionSerializer(
                order.product_version, context={"all_runs": True}
            ).data,
            "item_price": str(order.per_item_price),
            "total_price": str(order.total_price),
            "status": order.status,
            "discount": None,
            "created_on": order.created_on,
            "reference_number": order.reference_number,
            "coupon_code": order.coupon.coupon_code if order.coupon else None,
            "contract_number": order.contract_number,
            "receipt_data": {"card_type": None, "card_number": None},
        },
    )
Exemple #4
0
def test_program_run_enrollment_codes(client):
    """A CSV file with enrollment codes should be provided for the B2BOrder that is attached to a ProgramRun"""
    program_run = ProgramRunFactory()
    coupon_version = CouponVersionFactory.create()
    coupons = [
        coupon_version.coupon,
        CouponVersionFactory.create(
            payment_version=coupon_version.payment_version
        ).coupon,
    ]
    order = B2BOrderFactory.create(
        coupon_payment_version=coupon_version.payment_version, program_run=program_run
    )

    resp = client.get(reverse("b2b-enrollment-codes", kwargs={"hash": order.unique_id}))
    rows = [line.split(",") for line in resp.content.decode().split("\r\n")]
    assert sorted(rows[3 : len(rows) - 1]) == sorted(
        [
            [
                make_checkout_url(
                    code=coupon.coupon_code,
                    product_id=order.product_version.text_id,
                    run_tag=program_run.run_tag,
                )
            ]
            for coupon in coupons
        ]
    )
Exemple #5
0
def test_serialize_b2b_order(status, existing_user, user):
    """Test that B2BOrderToDealSerializer produces the correct serialized data"""
    order = B2BOrderFactory.create(status=status, num_seats=10)
    purchaser_id = order.email
    if existing_user:
        order.email = user.email
        purchaser_id = user.id
    serialized_data = B2BOrderToDealSerializer(instance=order).data
    assert serialized_data == {
        "id": order.id,
        "name": f"XPRO-B2BORDER-{order.id}",
        "stage": ORDER_STATUS_MAPPING[status],
        "amount": order.total_price.to_eng_string(),
        "discount_amount": None,
        "close_date": (
            int(order.updated_on.timestamp() * 1000)
            if status == Order.FULFILLED
            else None
        ),
        "coupon_code": None,
        "company": None,
        "payment_type": None,
        "payment_transaction": None,
        "discount_percent": None,
        "num_seats": 10,
        "status": order.status,
        "purchaser": format_hubspot_id(purchaser_id),
    }
Exemple #6
0
def test_b2b_order_audit():
    """
    B2BOrder.save_and_log() should save the order's information to an audit model.
    """
    order = B2BOrderFactory.create()
    assert B2BOrderAudit.objects.count() == 0
    order.save_and_log(None)

    assert B2BOrderAudit.objects.count() == 1
    order_audit = B2BOrderAudit.objects.first()
    assert order_audit.order == order

    assert order_audit.data_after == {
        **serialize_model_object(order),
        "product_version_info": {
            **serialize_model_object(order.product_version),
            "product_info": {
                **serialize_model_object(order.product_version.product),
                "content_type_string": str(order.product_version.product.content_type),
                "content_object": serialize_model_object(
                    order.product_version.product.content_object
                ),
            },
        },
        "receipts": [
            serialize_model_object(receipt) for receipt in order.b2breceipt_set.all()
        ],
    }
Exemple #7
0
def test_enrollment_codes(client):
    """A CSV file with enrollment codes should be provided for the B2BOrder"""
    coupon_version = CouponVersionFactory.create()
    coupons = [coupon_version.coupon] + [
        CouponVersionFactory.create(
            payment_version=coupon_version.payment_version
        ).coupon
        for _ in range(5)
    ]
    order = B2BOrderFactory.create(
        coupon_payment_version=coupon_version.payment_version
    )

    resp = client.get(reverse("b2b-enrollment-codes", kwargs={"hash": order.unique_id}))
    assert resp.status_code == status.HTTP_200_OK
    assert resp.get("Content-Type") == "text/csv"
    assert (
        resp.get("Content-Disposition")
        == f'attachment; filename="enrollmentcodes-{order.unique_id}.csv"'
    )
    rows = [line.split(",") for line in resp.content.decode().split("\r\n")]
    assert rows[0] == [
        "Distribute the links below to each of your learners. Additional instructions are available at:"
    ]
    assert sorted(rows[3 : len(rows) - 1]) == sorted(
        [
            [
                make_checkout_url(
                    code=coupon.coupon_code, product_id=order.product_version.text_id
                )
            ]
            for coupon in coupons
        ]
    )
Exemple #8
0
def test_get_new_b2b_order_by_reference_number():
    """
    get_new_order_by_reference_number returns an Order with status created
    """
    order = B2BOrderFactory.create(status=B2BOrder.CREATED)
    same_order = B2BOrder.objects.get_by_reference_number(
        order.reference_number)
    assert same_order.id == order.id
Exemple #9
0
def test_signed_payload(mocker, contract_number):
    """
    A valid payload should be signed appropriately
    """
    order = B2BOrderFactory.create(contract_number=contract_number)
    transaction_uuid = "hex"

    now = now_in_utc()
    now_mock = mocker.patch("b2b_ecommerce.api.now_in_utc",
                            autospec=True,
                            return_value=now)
    product_version = order.product_version
    product = product_version.product

    mocker.patch(
        "b2b_ecommerce.api.uuid.uuid4",
        autospec=True,
        return_value=mocker.MagicMock(hex=transaction_uuid),
    )
    receipt_url = "https://example.com/base_url/receipt/"
    cancel_url = "https://example.com/base_url/cancel/"
    payload = generate_b2b_cybersource_sa_payload(order=order,
                                                  receipt_url=receipt_url,
                                                  cancel_url=cancel_url)
    signature = payload.pop("signature")
    assert generate_cybersource_sa_signature(payload) == signature
    signed_field_names = payload["signed_field_names"].split(",")
    assert signed_field_names == sorted(payload.keys())

    total_price = order.total_price
    assert payload == {
        "access_key": CYBERSOURCE_ACCESS_KEY,
        "amount": str(total_price),
        "currency": "USD",
        "item_0_code": "enrollment_code",
        "item_0_name":
        f"Enrollment codes for {product_version.description}"[:254],
        "item_0_quantity": order.num_seats,
        "item_0_sku":
        f"enrollment_code-{str(product.content_type)}-{product.content_object.id}",
        "item_0_tax_amount": "0",
        "item_0_unit_price": str(total_price),
        "line_item_count": 1,
        "locale": "en-us",
        "reference_number": order.reference_number,
        "override_custom_receipt_page": receipt_url,
        "override_custom_cancel_page": cancel_url,
        "profile_id": CYBERSOURCE_PROFILE_ID,
        "signed_date_time": now.strftime(ISO_8601_FORMAT),
        "signed_field_names": ",".join(signed_field_names),
        "transaction_type": "sale",
        "transaction_uuid": transaction_uuid,
        "unsigned_field_names": "",
        "merchant_defined_data1": order.contract_number or "",
    }
    now_mock.assert_called_once_with()
Exemple #10
0
def test_send_b2b_receipt_email_error(mocker):
    """send_b2b_receipt_email should log an error and silence the exception if sending mail fails"""
    order = B2BOrderFactory.create()
    patched_mail_api = mocker.patch("ecommerce.mail_api.api")
    patched_log = mocker.patch("ecommerce.mail_api.log")
    patched_mail_api.send_message.side_effect = Exception("error")

    send_b2b_receipt_email(order)

    patched_log.exception.assert_called_once_with(
        "Error sending receipt email")
Exemple #11
0
def test_reference_number(settings):
    """
    order.reference_number should concatenate the reference prefix and the order id
    """
    settings.ENVIRONMENT = "test"

    order = B2BOrderFactory.create()
    assert (
        f"{REFERENCE_NUMBER_PREFIX}{settings.ENVIRONMENT}-{order.id}"
        == order.reference_number
    )
Exemple #12
0
def test_serialize_b2b_product_version():
    """Test that B2BProductVersionToLineSerializer produces the correct serialized data"""
    order = B2BOrderFactory.create(status=Order.FULFILLED, num_seats=10)
    serialized_data = B2BProductVersionToLineSerializer(instance=order).data
    assert serialized_data == {
        "id": format_hubspot_id(order.product_version.id),
        "product": format_hubspot_id(order.product_version.product.id),
        "order": format_hubspot_id(order.integration_id),
        "quantity": order.num_seats,
        "status": order.status,
        "product_id": order.product_version.text_id,
    }
Exemple #13
0
def test_save_and_log_order(mocker):
    """
    Tests that the save_model() function on B2BOrderAdmin creates an B2BOrderAudit entry
    """
    assert B2BOrderAudit.objects.count() == 0
    order = B2BOrderFactory.create()
    admin = B2BOrderAdmin(model=order, admin_site=mocker.Mock())
    admin.save_model(
        request=mocker.Mock(user=None),
        obj=admin.model,
        form=mocker.Mock(),
        change=mocker.Mock(),
    )
    assert B2BOrderAudit.objects.count() == 1
Exemple #14
0
def test_not_accept(decision):
    """
    If the decision is not ACCEPT then the order should be marked as failed
    """
    order = B2BOrderFactory.create(status=B2BOrder.CREATED)

    data = {
        "req_reference_number": order.reference_number,
        "decision": decision
    }
    fulfill_b2b_order(data)
    order.refresh_from_db()
    assert B2BOrder.objects.count() == 1
    assert order.status == B2BOrder.FAILED
Exemple #15
0
def test_ignore_duplicate_cancel():
    """
    If the decision is CANCEL and we already have a duplicate failed order, don't change anything.
    """
    order = B2BOrderFactory.create(status=B2BOrder.FAILED)

    data = {
        "req_reference_number": order.reference_number,
        "decision": "CANCEL"
    }
    fulfill_b2b_order(data)

    assert B2BOrder.objects.count() == 1
    assert B2BOrder.objects.get(id=order.id).status == B2BOrder.FAILED
Exemple #16
0
def test_create_order_duplicate_reference_number(client):
    """
    A 400 error should be returned if reference_number is duplicate
    """
    duplicate_test_contract_number = "DUPLICATE_TEST"
    B2BOrderFactory.create(status=B2BOrder.FULFILLED,
                           contract_number=duplicate_test_contract_number)
    resp = client.post(
        reverse("b2b-checkout"),
        {
            "num_seats": 1,
            "email": "*****@*****.**",
            "product_version_id": 987,
            "discount_code": "",
            "contract_number": duplicate_test_contract_number.lower(
            ),  # additional step for case insensitivity
        },
    )
    assert resp.status_code == status.HTTP_400_BAD_REQUEST
    assert resp.json() == {
        "errors": {
            "contract_number": "This contract number has already been used"
        }
    }
Exemple #17
0
def test_coupon_view_product_with_text_id(client):
    """Information about a coupon should be returned with product text id"""
    order = B2BOrderFactory.create(status=B2BOrder.CREATED)
    coupon = B2BCouponFactory.create(product=order.product_version.product)
    B2BCouponRedemption.objects.create(coupon=coupon, order=order)
    response = client.get(
        f'{reverse("b2b-coupon-view")}?'
        f'{urlencode({"code": coupon.coupon_code, "product_id": order.product_version.text_id})}'
    )
    assert response.status_code == status.HTTP_200_OK
    assert response.json() == {
        "code": coupon.coupon_code,
        "discount_percent": str(coupon.discount_percent),
        "product_id": order.product_version.product_id,
    }
Exemple #18
0
def test_error_on_duplicate_order(mocker, order_status, decision):
    """If there is a duplicate message (except for CANCEL), raise an exception"""
    order = B2BOrderFactory.create(status=order_status)

    data = {
        "req_reference_number": order.reference_number,
        "decision": decision
    }
    mocker.patch("ecommerce.views.IsSignedByCyberSource.has_permission",
                 return_value=True)
    with pytest.raises(EcommerceException) as ex:
        fulfill_b2b_order(data)

    assert B2BOrder.objects.count() == 1
    assert B2BOrder.objects.get(id=order.id).status == order_status

    assert ex.value.args[0] == f"{order} is expected to have status 'created'"
Exemple #19
0
def test_reusable_coupon_order_fulfilled(client, reusable, order_status):
    """Information about a reusable coupon should be returned"""
    order = B2BOrderFactory.create(status=order_status)
    coupon = B2BCouponFactory.create(product=None, reusable=reusable)
    B2BCouponRedemption.objects.create(coupon=coupon, order=order)
    response = client.get(
        f'{reverse("b2b-coupon-view")}?'
        f'{urlencode({"code": coupon.coupon_code, "product_id": order.product_version.product_id})}'
    )

    if reusable:
        assert response.status_code == status.HTTP_200_OK
        assert response.json() == {
            "code": coupon.coupon_code,
            "discount_percent": str(coupon.discount_percent),
            "product_id": order.product_version.product_id,
        }
    else:
        assert response.status_code == status.HTTP_404_NOT_FOUND
Exemple #20
0
def test_send_b2b_receipt_email(mocker, settings, has_discount):
    """send_b2b_receipt_email should send a receipt email"""
    patched_mail_api = mocker.patch("ecommerce.mail_api.api")
    order = B2BOrderFactory.create()
    if has_discount:
        discount = order.total_price / 3
        order.discount = discount
        order.total_price -= discount
        order.save()

    send_b2b_receipt_email(order)

    run = order.product_version.product.content_object
    download_url = f'{urljoin(settings.SITE_BASE_URL, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}'

    patched_mail_api.context_for_user.assert_called_once_with(
        user=None,
        extra_context={
            "purchase_date": order.updated_on.strftime(EMAIL_DATE_FORMAT),
            "total_price": format_price(order.total_price),
            "item_price": format_price(order.per_item_price),
            "discount": format_price(order.discount) if has_discount else None,
            "num_seats": str(order.num_seats),
            "contract_number": order.contract_number,
            "readable_id": get_readable_id(run),
            "run_date_range":
            f"{run.start_date.strftime(EMAIL_DATE_FORMAT)} - {run.end_date.strftime(EMAIL_DATE_FORMAT)}",
            "title": run.title,
            "download_url": download_url,
            "email": order.email,
            "order_reference_id": order.reference_number,
            "receipt_data": {},
        },
    )
    patched_mail_api.message_for_recipient.assert_called_once_with(
        order.email, patched_mail_api.context_for_user.return_value,
        EMAIL_B2B_RECEIPT)
    patched_mail_api.send_message.assert_called_once_with(
        patched_mail_api.message_for_recipient.return_value)
Exemple #21
0
def test_complete_b2b_order(mocker, contract_number, b2b_coupon_code):
    """
    complete_b2b_order should create the coupons and also send an email for the receipt
    """

    if b2b_coupon_code:
        b2b_coupon = B2BCouponFactory.create(coupon_code=b2b_coupon_code)
    else:
        b2b_coupon = None
    order = B2BOrderFactory.create(contract_number=contract_number,
                                   coupon=b2b_coupon)
    payment_version = CouponPaymentVersionFactory.create()
    send_email_mock = mocker.patch("b2b_ecommerce.api.send_b2b_receipt_email")
    create_coupons = mocker.patch("b2b_ecommerce.api.create_coupons",
                                  return_value=payment_version)

    if b2b_coupon and contract_number:
        expected_name = f"order_{order.id} {contract_number} {b2b_coupon.coupon_code}"
    elif contract_number:
        expected_name = contract_number
    elif b2b_coupon:
        expected_name = b2b_coupon.coupon_code
    else:
        expected_name = f"CouponPayment for order #{order.id}"

    complete_b2b_order(order)
    order.refresh_from_db()
    assert order.coupon_payment_version == payment_version
    create_coupons.assert_called_once_with(
        name=expected_name,
        product_ids=[order.product_version.product.id],
        amount=Decimal("1"),
        num_coupon_codes=order.num_seats,
        coupon_type=CouponPaymentVersion.SINGLE_USE,
        payment_type=CouponPaymentVersion.PAYMENT_SALE,
        payment_transaction=order.contract_number or order.reference_number,
    )
    send_email_mock.assert_called_once_with(order)
Exemple #22
0
def order_with_coupon():
    """Create a unfulfilled order with a coupon which is valid for it"""
    order = B2BOrderFactory.create(status=B2BOrder.CREATED)
    coupon = B2BCouponFactory.create(product=order.product_version.product)
    B2BCouponRedemption.objects.create(coupon=coupon, order=order)
    return SimpleNamespace(order=order, coupon=coupon)