Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New Create Order API #7047

Merged
merged 4 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions app/api/attendees.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']:
Expand Down
114 changes: 30 additions & 84 deletions app/api/custom/orders.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'json' imported but unused

from datetime import datetime

import pytz
Expand Down Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local variable 'event' is assigned to but never used


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/<order_id>', methods=['PATCH'])
Expand Down
2 changes: 1 addition & 1 deletion app/api/helpers/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
17 changes: 17 additions & 0 deletions app/models/ticket.py
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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 '<Ticket %r>' % self.name

Expand Down
1 change: 1 addition & 0 deletions app/templates/flask_ext/jinja/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
106 changes: 106 additions & 0 deletions tests/all/integration/api/helpers/order/test_create_order.py
Original file line number Diff line number Diff line change
@@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'tests.factories.order.OrderFactory' imported but unused

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)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Black would make changes.



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"},
}