From db012a470dc0170e2497db15323d88d474c6a359 Mon Sep 17 00:00:00 2001 From: iamareebjamal Date: Thu, 4 Jun 2020 15:23:24 +0530 Subject: [PATCH 1/4] wip: New Order create API --- app/api/attendees.py | 5 +- app/api/custom/orders.py | 114 +++++------------- app/api/helpers/order.py | 2 +- app/models/ticket.py | 17 +++ app/templates/flask_ext/jinja/filters.py | 1 + .../api/helpers/order/test_create_order.py | 106 ++++++++++++++++ 6 files changed, 156 insertions(+), 89 deletions(-) create mode 100644 tests/all/integration/api/helpers/order/test_create_order.py diff --git a/app/api/attendees.py b/app/api/attendees.py index 44f2e20503..69ab8e0332 100644 --- a/app/api/attendees.py +++ b/app/api/attendees.py @@ -85,10 +85,7 @@ def before_post(self, args, kwargs, data): "Ticket belongs to a different Event", ) # Check if the ticket is already sold out or not. - if get_sold_and_reserved_tickets_count(ticket.id) >= ticket.quantity: - raise ConflictError( - {'pointer': '/data/attributes/ticket_id'}, "Ticket already sold out" - ) + ticket.raise_if_unavailable() if 'device_name_checkin' in data and data['device_name_checkin'] is not None: if 'is_checked_in' not in data or not data['is_checked_in']: diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py index 5f0c90ca5a..1ffd947cf5 100644 --- a/app/api/custom/orders.py +++ b/app/api/custom/orders.py @@ -1,3 +1,4 @@ +import json from datetime import datetime import pytz @@ -117,100 +118,45 @@ def resend_emails(): @order_blueprint.route('/calculate-amount', methods=['POST']) def calculate_amount(): data, errors = OrderAmountInputSchema().load(request.get_json()) + if errors: + return make_response(jsonify(errors), 422) return jsonify(calculate_order_amount(data['tickets'], data.get('discount_code'))) @order_blueprint.route('/create-order', methods=['POST']) @jwt_required def create_order(): - data = request.get_json() - tickets, discount_code = data['tickets'], data['discount-code'] - attendee = data['attendee'] - for attribute in attendee: - attendee[attribute.replace('-', '_')] = attendee.pop(attribute) - schema = AttendeeSchema() - json_api_attendee = {"data": {"attributes": data['attendee'], "type": "attendee"}} - result = schema.load(json_api_attendee) - if result.errors: - return make_response(jsonify(result.errors), 422) - ticket_ids = {int(ticket['id']) for ticket in tickets} - quantity = {int(ticket['id']): ticket['quantity'] for ticket in tickets} - ticket_list = ( - db.session.query(Ticket) - .filter(Ticket.id.in_(ticket_ids)) - .filter_by(event_id=data['event_id'], deleted_at=None) - .all() - ) - ticket_ids_found = {ticket_information.id for ticket_information in ticket_list} - tickets_not_found = ticket_ids - ticket_ids_found - if tickets_not_found: - return make_response( - jsonify( - status='Order Unsuccessful', - error='Tickets with id {} were not found in Event {}.'.format( - tickets_not_found, data['event_id'] - ), - ), - 404, - ) - for ticket_info in ticket_list: - if ( - ticket_info.quantity - - get_count( - db.session.query(TicketHolder.id).filter_by( - ticket_id=int(ticket_info.id), deleted_at=None - ) - ) - ) < quantity[ticket_info.id]: - return make_response( - jsonify(status='Order Unsuccessful', error='Ticket already sold out.'), - 409, - ) - attendee_list = [] - for ticket in tickets: - for _ in range(ticket['quantity']): - attendee = TicketHolder( - **result[0], event_id=int(data['event_id']), ticket_id=int(ticket['id']) - ) - db.session.add(attendee) - attendee_list.append(attendee) - # created_at not getting filled - ticket_pricing = calculate_order_amount(tickets, discount_code) - if not has_access('is_coorganizer', event_id=data['event_id']): - data['status'] = 'initializing' - # create on site attendees - # check if order already exists for this attendee. - # check for free tickets and verified user - order = Order( - amount=ticket_pricing['total_amount'], - user_id=current_user.id, - event_id=int(data['event_id']), - status=data['status'], + data, errors = OrderAmountInputSchema().load(request.get_json()) + if errors: + return make_response(jsonify(errors), 422) + + tickets_dict = data['tickets'] + order_amount = calculate_order_amount(tickets_dict, data.get('discount_code')) + ticket_ids = {ticket['id'] for ticket in tickets_dict} + ticket_map = {int(ticket['id']): ticket for ticket in tickets_dict} + tickets = ( + Ticket.query.filter_by(deleted_at=None).filter(Ticket.id.in_(ticket_ids)).all() ) - db.session.add(order) - db.session.commit() - db.session.refresh(order) - order_tickets = {} - for holder in attendee_list: - holder.order_id = order.id - db.session.add(holder) - if not order_tickets.get(holder.ticket_id): - order_tickets[holder.ticket_id] = 1 - else: - order_tickets[holder.ticket_id] += 1 - for ticket in order_tickets: - od = OrderTicket( - order_id=order.id, ticket_id=ticket, quantity=order_tickets[ticket] + if not tickets: + raise UnprocessableEntityError( + {'source': 'tickets'}, "Tickets missing in Order request", ) - db.session.add(od) - order.quantity = order.tickets_count - db.session.add(order) - db.session.commit() - db.session.refresh(order) - order_schema = OrderSchema() - return order_schema.dump(order) + event = tickets[0].event + + try: + attendees = [] + for ticket in tickets: + for _ in range(ticket_map[ticket.id]['quantity']): + ticket.raise_if_unavailable() + attendees.append(TicketHolder(firstname='', lastname='', ticket=ticket)) + db.session.commit() + except Exception as e: + db.session.rollback() + raise e + + return jsonify(order_amount) @order_blueprint.route('/complete-order/', methods=['PATCH']) diff --git a/app/api/helpers/order.py b/app/api/helpers/order.py index 1ccd71fda0..c4cbf0fbb4 100644 --- a/app/api/helpers/order.py +++ b/app/api/helpers/order.py @@ -186,7 +186,7 @@ def calculate_order_amount(tickets, discount_code=None): from app.models.discount_code import DiscountCode from app.api.helpers.ticketing import validate_tickets, validate_discount_code - ticket_ids = [ticket['id'] for ticket in tickets] + ticket_ids = {ticket['id'] for ticket in tickets} ticket_map = {int(ticket['id']): ticket for ticket in tickets} fetched_tickets = validate_tickets(ticket_ids) diff --git a/app/models/ticket.py b/app/models/ticket.py index 3afb10cb8c..bdfda84b5a 100644 --- a/app/models/ticket.py +++ b/app/models/ticket.py @@ -1,6 +1,7 @@ from app.models import db from app.models.base import SoftDeletionModel from app.models.order import Order, OrderTicket +from app.api.helpers.errors import ConflictError access_codes_tickets = db.Table( 'access_codes_tickets', @@ -108,6 +109,22 @@ def tags_csv(self): tag_names = [tag.name for tag in self.tags] return ','.join(tag_names) + @property + def reserved_count(self): + from app.api.attendees import get_sold_and_reserved_tickets_count + + return get_sold_and_reserved_tickets_count(self.id) + + @property + def is_available(self): + sold = self.reserved_count + print(sold, self.quantity) + return sold < self.quantity + + def raise_if_unavailable(self): + if not self.is_available: + raise ConflictError({'id': self.id}, f"Ticket {self.id} already sold out") + def __repr__(self): return '' % self.name diff --git a/app/templates/flask_ext/jinja/filters.py b/app/templates/flask_ext/jinja/filters.py index 240f62a50e..d96fd986a8 100644 --- a/app/templates/flask_ext/jinja/filters.py +++ b/app/templates/flask_ext/jinja/filters.py @@ -12,6 +12,7 @@ def humanize_helper(time): return "N/A" return humanize.naturaltime(datetime.now(pytz.utc) - time.astimezone(pytz.utc)) + def init_filters(app): @app.template_filter('currency_symbol') def currency_symbol_filter(currency_code): diff --git a/tests/all/integration/api/helpers/order/test_create_order.py b/tests/all/integration/api/helpers/order/test_create_order.py new file mode 100644 index 0000000000..67d1ab9423 --- /dev/null +++ b/tests/all/integration/api/helpers/order/test_create_order.py @@ -0,0 +1,106 @@ +import json + +import pytest + +from tests.factories.discount_code import DiscountCodeTicketSubFactory +from .test_calculate_order_amount import _create_taxed_tickets, _create_tickets +from tests.factories.user import UserFactory +from flask_jwt_extended.utils import create_access_token +from app.models.ticket_holder import TicketHolder +from tests.factories.event import EventFactoryBasic +from tests.factories.order import OrderFactory, OrderSubFactory +from tests.factories.attendee import AttendeeFactoryBase + + +@pytest.fixture +def jwt(db): + user = UserFactory() + db.session.commit() + + return {'Authorization': "JWT " + create_access_token(user.id, fresh=True)} + + +def test_create_order(client, db, jwt): + discount_code = DiscountCodeTicketSubFactory(type='percent', value=10.0, tickets=[]) + tickets_dict = _create_taxed_tickets( + db, tax_included=False, discount_code=discount_code + ) + db.session.commit() + + response = client.post( + '/v1/orders/create-order', + content_type='application/json', + headers=jwt, + data=json.dumps( + {'tickets': tickets_dict, 'discount-code': str(discount_code.id)} + ), + ) + + assert TicketHolder.query.count() == 12 + + assert response.status_code == 200 + amount_data = json.loads(response.data) + assert amount_data['sub_total'] == 4021.87 + assert amount_data['total'] == 4745.81 + assert amount_data['tax']['included'] is False + assert amount_data['tax']['amount'] == 723.94 + + +def test_throw_ticket_sold_out(client, db, jwt): + event = EventFactoryBasic() + tickets = _create_tickets([10, 20], event=event, quantity=2) + order = OrderSubFactory(status='completed', event=event) + AttendeeFactoryBase.create_batch(2, order=order, ticket=tickets[0], event=event) + AttendeeFactoryBase.create_batch(2, order=order, ticket=tickets[1], event=event) + db.session.commit() + + response = client.post( + '/v1/orders/create-order', + content_type='application/json', + headers=jwt, + data=json.dumps( + { + 'tickets': [ + {'id': tickets[0].id, 'quantity': 2}, + {'id': tickets[1].id, 'quantity': 3}, + ] + } + ), + ) + + assert TicketHolder.query.count() == 0 + + assert response.status_code == 409 + assert json.loads(response.data) == { + 'errors': [ + { + 'detail': 'Ticket 5 already sold out', + 'source': {'id': 5}, + 'status': 409, + 'title': 'Conflict', + } + ], + 'jsonapi': {'version': '1.0'}, + } + + +def test_throw_empty_tickets(client, db, jwt): + response = client.post( + '/v1/orders/create-order', + content_type='application/json', + headers=jwt, + data=json.dumps({'tickets': []}), + ) + + assert response.status_code == 422 + assert json.loads(response.data) == { + "errors": [ + { + "status": 422, + "source": {"source": "tickets"}, + "title": "Unprocessable Entity", + "detail": "Tickets missing in Order request", + } + ], + "jsonapi": {"version": "1.0"}, + } From b57ac77129d8ee09035a1e9e89a628b5c6045b4f Mon Sep 17 00:00:00 2001 From: iamareebjamal Date: Thu, 4 Jun 2020 18:49:55 +0530 Subject: [PATCH 2/4] wip --- app/api/custom/orders.py | 17 +- app/api/orders.py | 188 +++++++++--------- app/models/order.py | 9 +- .../api/helpers/order/test_create_order.py | 59 ++++-- .../all/integration/api/helpers/test_order.py | 8 +- tests/factories/order.py | 2 +- 6 files changed, 157 insertions(+), 126 deletions(-) diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py index 1ffd947cf5..1cc0aed666 100644 --- a/app/api/custom/orders.py +++ b/app/api/custom/orders.py @@ -2,11 +2,12 @@ from datetime import datetime import pytz -from flask import Blueprint, jsonify, make_response, request +from flask import Blueprint, app, jsonify, make_response, request from flask_jwt_extended import current_user, jwt_required from sqlalchemy.orm.exc import NoResultFound from app.api.auth import return_file +from app.api.custom.schema.order_amount import OrderAmountInputSchema from app.api.helpers.db import get_count, safe_query from app.api.helpers.errors import ForbiddenError, NotFoundError, UnprocessableEntityError from app.api.helpers.files import make_frontend_url @@ -19,7 +20,7 @@ from app.api.helpers.order import calculate_order_amount, create_pdf_tickets_for_holder from app.api.helpers.permission_manager import has_access from app.api.helpers.storage import UPLOAD_PATHS, generate_hash -from app.api.custom.schema.order_amount import OrderAmountInputSchema +from app.api.orders import validate_attendees from app.api.schema.attendees import AttendeeSchema from app.api.schema.orders import OrderSchema from app.extensions.limiter import limiter @@ -156,7 +157,17 @@ def create_order(): db.session.rollback() raise e - return jsonify(order_amount) + validate_attendees({attendee.id for attendee in attendees}) + order = Order( + amount=order_amount['total'], + event=event, + discount_code_id=data.get('discount_code'), + ticket_holders=attendees, + ) + db.session.commit() + order.populate_and_save() + + return OrderSchema().dumps(order) @order_blueprint.route('/complete-order/', methods=['PATCH']) diff --git a/app/api/orders.py b/app/api/orders.py index 4f326727b7..19b459b49b 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -112,6 +112,96 @@ def check_billing_info(data): ) +def on_order_completed(order): + # send e-mail and notifications if the order status is completed + if not (order.status == 'completed' or order.status == 'placed'): + return + # fetch tickets attachment + order_identifier = order.identifier + + key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier) + ticket_path = ( + 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + + order_identifier + + '.pdf' + ) + + key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier) + invoice_path = ( + 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + + order_identifier + + '.pdf' + ) + + # send email and notifications. + send_email_to_attendees( + order=order, + purchaser_id=current_user.id, + attachments=[ticket_path, invoice_path], + ) + + send_notif_to_attendees(order, current_user.id) + + if order.payment_mode in ['free', 'bank', 'cheque', 'onsite']: + order.completed_at = datetime.utcnow() + + order_url = make_frontend_url( + path='/orders/{identifier}'.format(identifier=order.identifier) + ) + for organizer in set( + order.event.organizers + order.event.coorganizers + [order.event.owner] + ): + if not organizer: + continue + send_notif_ticket_purchase_organizer( + organizer, + order.invoice_number, + order_url, + order.event.name, + order.identifier, + ) + + +def save_order(order): + order_tickets = {} + for holder in order.ticket_holders: + save_to_db(holder) + if not order_tickets.get(holder.ticket_id): + order_tickets[holder.ticket_id] = 1 + else: + order_tickets[holder.ticket_id] += 1 + + order.user = current_user + + # create pdf tickets. + create_pdf_tickets_for_holder(order) + + for ticket in order_tickets: + od = OrderTicket( + order_id=order.id, ticket_id=ticket, quantity=order_tickets[ticket] + ) + save_to_db(od) + + order.quantity = order.tickets_count + save_to_db(order) + + on_order_completed(order) + + +def validate_attendees(ticket_holders): + free_ticket_quantity = 0 + + for ticket_holder in validate_ticket_holders(ticket_holders): + if ticket_holder.ticket.type == 'free': + free_ticket_quantity += 1 + + if not current_user.is_verified and free_ticket_quantity == len(ticket_holders): + raise ForbiddenError( + {'pointer': '/data/relationships/user', 'code': 'unverified-user'}, + "Unverified user cannot place free orders", + ) + + class OrdersListPost(ResourceList): """ OrderListPost class for OrderSchema @@ -145,20 +235,7 @@ def before_create_object(self, data, view_kwargs): :param view_kwargs: :return: """ - - free_ticket_quantity = 0 - - for ticket_holder in validate_ticket_holders(data['ticket_holders']): - if ticket_holder.ticket.type == 'free': - free_ticket_quantity += 1 - - if not current_user.is_verified and free_ticket_quantity == len( - data['ticket_holders'] - ): - raise ForbiddenError( - {'pointer': '/data/relationships/user', 'code': 'unverified-user'}, - "Unverified user cannot place free orders", - ) + validate_attendees(data['ticket_holders']) if data.get('cancel_note'): del data['cancel_note'] @@ -189,88 +266,7 @@ def after_create_object(self, order, data, view_kwargs): :param view_kwargs: :return: """ - order_tickets = {} - for holder in order.ticket_holders: - save_to_db(holder) - if not order_tickets.get(holder.ticket_id): - order_tickets[holder.ticket_id] = 1 - else: - order_tickets[holder.ticket_id] += 1 - - order.user = current_user - - # create pdf tickets. - create_pdf_tickets_for_holder(order) - - for ticket in order_tickets: - od = OrderTicket( - order_id=order.id, ticket_id=ticket, quantity=order_tickets[ticket] - ) - save_to_db(od) - - order.quantity = order.tickets_count - save_to_db(order) - # if not has_access('is_coorganizer', event_id=data['event']): - # TicketingManager.calculate_update_amount(order) - - # send e-mail and notifications if the order status is completed - if order.status == 'completed' or order.status == 'placed': - # fetch tickets attachment - order_identifier = order.identifier - - key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier) - ticket_path = ( - 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) - + order_identifier - + '.pdf' - ) - - key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier) - invoice_path = ( - 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) - + order_identifier - + '.pdf' - ) - - # send email and notifications. - send_email_to_attendees( - order=order, - purchaser_id=current_user.id, - attachments=[ticket_path, invoice_path], - ) - - send_notif_to_attendees(order, current_user.id) - - if order.payment_mode in ['free', 'bank', 'cheque', 'onsite']: - order.completed_at = datetime.utcnow() - - order_url = make_frontend_url( - path='/orders/{identifier}'.format(identifier=order.identifier) - ) - for organizer in order.event.organizers: - send_notif_ticket_purchase_organizer( - organizer, - order.invoice_number, - order_url, - order.event.name, - order.identifier, - ) - for coorganizer in order.event.coorganizers: - send_notif_ticket_purchase_organizer( - coorganizer, - order.invoice_number, - order_url, - order.event.name, - order.identifier, - ) - if order.event.owner: - send_notif_ticket_purchase_organizer( - order.event.owner, - order.invoice_number, - order_url, - order.event.name, - order.identifier, - ) + save_order(order) data['user_id'] = current_user.id diff --git a/app/models/order.py b/app/models/order.py index 55d48a0fae..71707d66bb 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -85,7 +85,7 @@ class Order(SoftDeletionModel): last4 = db.Column(db.String) stripe_token = db.Column(db.String) paypal_token = db.Column(db.String) - status = db.Column(db.String, default='pending') + status = db.Column(db.String, default='initializing') cancel_note = db.Column(db.String, nullable=True) order_notes = db.Column(db.String) tickets_pdf_url = db.Column(db.String) @@ -134,3 +134,10 @@ def get_revenue(self): ) else: return 0.0 + + # Saves the order and generates and sends appropriate + # documents and notifications + def populate_and_save(self): + from app.api.orders import save_order + + save_order(self) diff --git a/tests/all/integration/api/helpers/order/test_create_order.py b/tests/all/integration/api/helpers/order/test_create_order.py index 67d1ab9423..94d74ffd1b 100644 --- a/tests/all/integration/api/helpers/order/test_create_order.py +++ b/tests/all/integration/api/helpers/order/test_create_order.py @@ -1,15 +1,17 @@ import json import pytest - -from tests.factories.discount_code import DiscountCodeTicketSubFactory -from .test_calculate_order_amount import _create_taxed_tickets, _create_tickets -from tests.factories.user import UserFactory from flask_jwt_extended.utils import create_access_token + +from app.models.order import Order from app.models.ticket_holder import TicketHolder +from tests.factories.attendee import AttendeeFactoryBase +from tests.factories.discount_code import DiscountCodeTicketSubFactory from tests.factories.event import EventFactoryBasic from tests.factories.order import OrderFactory, OrderSubFactory -from tests.factories.attendee import AttendeeFactoryBase +from tests.factories.user import UserFactory + +from .test_calculate_order_amount import _create_taxed_tickets, _create_tickets @pytest.fixture @@ -39,11 +41,34 @@ def test_create_order(client, db, jwt): assert TicketHolder.query.count() == 12 assert response.status_code == 200 - amount_data = json.loads(response.data) - assert amount_data['sub_total'] == 4021.87 - assert amount_data['total'] == 4745.81 - assert amount_data['tax']['included'] is False - assert amount_data['tax']['amount'] == 723.94 + order_dict = json.loads(response.data) + order = Order.query.get(order_dict['data']['id']) + assert order_dict['data']['attributes']['amount'] == 4745.81 + assert order.amount == 4745.81 + assert len(order.ticket_holders) == 12 + assert order.discount_code == discount_code + + +def test_create_order_without_discount(client, db, jwt): + tickets_dict = _create_taxed_tickets(db, tax_included=False) + db.session.commit() + + response = client.post( + '/v1/orders/create-order', + content_type='application/json', + headers=jwt, + data=json.dumps({'tickets': tickets_dict}), + ) + + assert TicketHolder.query.count() == 12 + + assert response.status_code == 200 + order_dict = json.loads(response.data) + order = Order.query.get(order_dict['data']['id']) + assert order_dict['data']['attributes']['amount'] == 5240.73 + assert order.amount == 5240.73 + assert len(order.ticket_holders) == 12 + assert order.discount_code is None def test_throw_ticket_sold_out(client, db, jwt): @@ -71,17 +96,9 @@ def test_throw_ticket_sold_out(client, db, jwt): assert TicketHolder.query.count() == 0 assert response.status_code == 409 - assert json.loads(response.data) == { - 'errors': [ - { - 'detail': 'Ticket 5 already sold out', - 'source': {'id': 5}, - 'status': 409, - 'title': 'Conflict', - } - ], - 'jsonapi': {'version': '1.0'}, - } + error_dict = json.loads(response.data) + assert error_dict['errors'][0]['title'] == 'Conflict' + assert 'already sold out' in error_dict['errors'][0]['detail'] def test_throw_empty_tickets(client, db, jwt): diff --git a/tests/all/integration/api/helpers/test_order.py b/tests/all/integration/api/helpers/test_order.py index d11a83e104..7cad0749cc 100644 --- a/tests/all/integration/api/helpers/test_order.py +++ b/tests/all/integration/api/helpers/test_order.py @@ -5,14 +5,14 @@ from app.api.attendees import get_sold_and_reserved_tickets_count from app.api.helpers.db import save_to_db from app.api.helpers.order import delete_related_attendees_for_order, set_expiry_for_order -from tests.factories.attendee import AttendeeFactoryBase -from tests.factories.event import EventFactoryBasic -from tests.factories.order import OrderFactory -from tests.factories.ticket import TicketFactory from app.models import db from app.models.order import Order from app.settings import get_settings from tests.all.integration.utils import OpenEventTestCase +from tests.factories.attendee import AttendeeFactoryBase +from tests.factories.event import EventFactoryBasic +from tests.factories.order import OrderFactory +from tests.factories.ticket import TicketFactory class TestOrderUtilities(OpenEventTestCase): diff --git a/tests/factories/order.py b/tests/factories/order.py index 41ef701f48..499b8aab87 100644 --- a/tests/factories/order.py +++ b/tests/factories/order.py @@ -1,8 +1,8 @@ import factory +from app.models.order import Order from tests.factories.base import BaseFactory from tests.factories.event import EventFactoryBasic -from app.models.order import Order class OrderFactoryBase(BaseFactory): From 85010f376094c3064caec4b55bbb07dfd7475f55 Mon Sep 17 00:00:00 2001 From: iamareebjamal Date: Fri, 5 Jun 2020 17:03:51 +0530 Subject: [PATCH 3/4] wip --- app/api/custom/orders.py | 3 +- app/models/ticket.py | 4 +- .../api/helpers/order/test_create_order.py | 37 +++++++++++++++++-- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py index 1cc0aed666..e388c71942 100644 --- a/app/api/custom/orders.py +++ b/app/api/custom/orders.py @@ -1,8 +1,7 @@ -import json from datetime import datetime import pytz -from flask import Blueprint, app, jsonify, make_response, request +from flask import Blueprint, jsonify, make_response, request from flask_jwt_extended import current_user, jwt_required from sqlalchemy.orm.exc import NoResultFound diff --git a/app/models/ticket.py b/app/models/ticket.py index bdfda84b5a..2e6a3b32dd 100644 --- a/app/models/ticket.py +++ b/app/models/ticket.py @@ -117,9 +117,7 @@ def reserved_count(self): @property def is_available(self): - sold = self.reserved_count - print(sold, self.quantity) - return sold < self.quantity + return self.reserved_count < self.quantity def raise_if_unavailable(self): if not self.is_available: diff --git a/tests/all/integration/api/helpers/order/test_create_order.py b/tests/all/integration/api/helpers/order/test_create_order.py index 94d74ffd1b..de6bf84614 100644 --- a/tests/all/integration/api/helpers/order/test_create_order.py +++ b/tests/all/integration/api/helpers/order/test_create_order.py @@ -8,15 +8,19 @@ from tests.factories.attendee import AttendeeFactoryBase from tests.factories.discount_code import DiscountCodeTicketSubFactory from tests.factories.event import EventFactoryBasic -from tests.factories.order import OrderFactory, OrderSubFactory +from tests.factories.order import OrderSubFactory from tests.factories.user import UserFactory -from .test_calculate_order_amount import _create_taxed_tickets, _create_tickets +from .test_calculate_order_amount import ( + _create_taxed_tickets, + _create_tickets, + _create_ticket_dict, +) @pytest.fixture def jwt(db): - user = UserFactory() + user = UserFactory(is_verified=False) db.session.commit() return {'Authorization': "JWT " + create_access_token(user.id, fresh=True)} @@ -121,3 +125,30 @@ def test_throw_empty_tickets(client, db, jwt): ], "jsonapi": {"version": "1.0"}, } + + +def test_throw_free_tickets(client, db, jwt): + tickets = _create_tickets([0, 0], event=EventFactoryBasic(), type='free') + db.session.commit() + response = client.post( + '/v1/orders/create-order', + content_type='application/json', + headers=jwt, + data=json.dumps({'tickets': _create_ticket_dict(tickets, [1, 2])}), + ) + + assert response.status_code == 403 + assert json.loads(response.data) == { + "errors": [ + { + "status": 403, + "source": { + "pointer": "/data/relationships/user", + "code": "unverified-user", + }, + "title": "Access Forbidden", + "detail": "Unverified user cannot place free orders", + } + ], + "jsonapi": {"version": "1.0"}, + } From 4e6d66c8e142c144d625064133c8304d24f91d7d Mon Sep 17 00:00:00 2001 From: iamareebjamal Date: Fri, 5 Jun 2020 17:33:59 +0530 Subject: [PATCH 4/4] wip --- app/api/attendees.py | 1 - app/api/custom/orders.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/attendees.py b/app/api/attendees.py index 69ab8e0332..e5080ab57b 100644 --- a/app/api/attendees.py +++ b/app/api/attendees.py @@ -7,7 +7,6 @@ from app.api.bootstrap import api from app.api.helpers.db import safe_query, safe_query_kwargs from app.api.helpers.errors import ( - ConflictError, ForbiddenError, UnprocessableEntityError, ) diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py index e388c71942..d5cf6d634a 100644 --- a/app/api/custom/orders.py +++ b/app/api/custom/orders.py @@ -20,12 +20,11 @@ from app.api.helpers.permission_manager import has_access from app.api.helpers.storage import UPLOAD_PATHS, generate_hash from app.api.orders import validate_attendees -from app.api.schema.attendees import AttendeeSchema from app.api.schema.orders import OrderSchema from app.extensions.limiter import limiter from app.models import db from app.models.custom_form import CustomForms -from app.models.order import Order, OrderTicket +from app.models.order import Order from app.models.ticket import Ticket from app.models.ticket_holder import TicketHolder