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)
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
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}, }, )
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 ] )
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), }
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() ], }
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 ] )
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
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()
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")
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 )
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, }
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
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
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
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" } }
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, }
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'"
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
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)
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)
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)