def customer_credit_info(): try: user = __get_user() """""" # @TODO : REFACTORING !!! from chalicelib.libs.purchase.customer.sqs import FbucksChargeSqsHandler see = FbucksChargeSqsHandler """""" # fbucks amount __fbucks_customer_amount_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT, ) fbucks_amount_row = __fbucks_customer_amount_elastic.get_data( user.id) fbucks_amount = fbucks_amount_row[ 'amount'] or 0 if fbucks_amount_row else 0 # fbucks history __fbucks_customer_amount_changes_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, ) fbucks_changes = __fbucks_customer_amount_changes_elastic.post_search( { "query": { "term": { "customer_id": user.id } } }).get('hits', {}).get('hits', []) or [] fbucks_changes = [ fbucks_change['_source'] for fbucks_change in fbucks_changes ] fbucks_changes = [{ 'amount': fbucks_change['amount'], 'changed_at': fbucks_change['changed_at'], 'order_number': fbucks_change['order_number'], } for fbucks_change in fbucks_changes] # cash out balance credit = CreditStorageImplementation().load(user.email) cash_out_balance = credit.paid if credit else 0 return { 'fbucks_amount': fbucks_amount, 'fbucks_changes': fbucks_changes, 'cache_out_balance': cash_out_balance, } except BaseException as e: return http_response_exception_or_throw(e)
class _CreditCardsElasticStorage(CreditCardsStorageInterface): __ENTITY_PROPERTY_TOKEN = '__token' __ENTITY_PROPERTY_CUSTOMER_ID = '__customer_id' __ENTITY_PROPERTY_BRAND = '__brand' __ENTITY_PROPERTY_NUMBER_HIDDEN = '__number_hidden' __ENTITY_PROPERTY_EXPIRES = '__expires' __ENTITY_PROPERTY_HOLDER_NAME = '__holder_name' __ENTITY_PROPERTY_IS_VERIFIED = '__is_verified' __ENTITY_PROPERTY_CREATED_AT = '__created_at' def __init__(self): """ curl -X DELETE localhost:9200/purchase_customer_credit_cards curl -X PUT localhost:9200/purchase_customer_credit_cards -H "Content-Type: application/json" -d'{ "mappings": { "purchase_customer_credit_cards": { "properties": { "token": {"type": "keyword"}, "customer_id": {"type": "keyword"}, "brand": {"type": "keyword"}, "number_hidden": {"type": "keyword"}, "expires": {"type": "keyword"}, //2005 -> 2020/05 "holder_name": {"type": "keyword"}, "is_verified": {"type": "boolean"}, "created_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"} } } } }' curl -X DELETE localhost:9200/purchase_customer_credit_cards_customer_map curl -X PUT localhost:9200/purchase_customer_credit_cards_customer_map -H "Content-Type: application/json" -d'{ "mappings": { "purchase_customer_credit_cards_customer_map": { "properties": { "tokens_json": {"type": "keyword"} } } } }' """ self.__elastic_cards = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_CUSTOMER_CREDIT_CARDS, settings.AWS_ELASTICSEARCH_PURCHASE_CUSTOMER_CREDIT_CARDS) self.__elastic_customer_cards_map = Elastic( settings. AWS_ELASTICSEARCH_PURCHASE_CUSTOMER_CREDIT_CARDS_CUSTOMER_MAP, settings. AWS_ELASTICSEARCH_PURCHASE_CUSTOMER_CREDIT_CARDS_CUSTOMER_MAP) self.__reflector = Reflector() def __restore(self, row: dict) -> CreditCard: card = self.__reflector.construct( CreditCard, { self.__class__.__ENTITY_PROPERTY_TOKEN: row['token'], self.__class__.__ENTITY_PROPERTY_CUSTOMER_ID: row['customer_id'], self.__class__.__ENTITY_PROPERTY_BRAND: row['brand'], self.__class__.__ENTITY_PROPERTY_NUMBER_HIDDEN: row['number_hidden'], self.__class__.__ENTITY_PROPERTY_EXPIRES: (datetime.date( year=int('20' + row['expires'][0:2]), month=12, day=31)) if int(row['expires'][2:4]) == 12 else (datetime.date(year=int('20' + row['expires'][0:2]), month=int(row['expires'][2:4]) + 1, day=1) - datetime.timedelta(days=1)), self.__class__.__ENTITY_PROPERTY_HOLDER_NAME: row['holder_name'], self.__class__.__ENTITY_PROPERTY_IS_VERIFIED: row['is_verified'], self.__class__.__ENTITY_PROPERTY_CREATED_AT: datetime.datetime.strptime(row['created_at'], '%Y-%m-%d %H:%M:%S') }) return card def get_by_token(self, token: str) -> Optional[CreditCard]: if not isinstance(token, str): raise ArgumentTypeException(self.get_by_token, 'token', token) elif not token.strip(): raise ArgumentCannotBeEmptyException(self.get_by_token, 'token') row = self.__elastic_cards.get_data(token) return self.__restore(row) if row else None def get_all_by_customer(self, customer_id: str) -> Tuple[CreditCard]: if not isinstance(customer_id, str): raise ArgumentTypeException(self.get_all_by_customer, 'customer_id', customer_id) elif not customer_id.strip(): raise ArgumentCannotBeEmptyException(self.get_all_by_customer, 'customer_id') customer_cards_map = self.__elastic_customer_cards_map.get_data( customer_id) tokens = json.loads( customer_cards_map['tokens_json']) if customer_cards_map else [] result = [self.get_by_token(token) for token in tokens] result = [card for card in result if card] if len(result) != len(tokens): raise ValueError( 'Incorrect cards set for customer #{}: existed cards - {}, tokens in map - {}' .format(customer_id, len(result), len(tokens))) return tuple(result) def save(self, card: CreditCard) -> None: if not isinstance(card, CreditCard): raise ArgumentTypeException(self.save, 'card', card) data = self.__reflector.extract(card, [ self.__class__.__ENTITY_PROPERTY_TOKEN, self.__class__.__ENTITY_PROPERTY_CUSTOMER_ID, self.__class__.__ENTITY_PROPERTY_BRAND, self.__class__.__ENTITY_PROPERTY_NUMBER_HIDDEN, self.__class__.__ENTITY_PROPERTY_EXPIRES, self.__class__.__ENTITY_PROPERTY_HOLDER_NAME, self.__class__.__ENTITY_PROPERTY_IS_VERIFIED, self.__class__.__ENTITY_PROPERTY_CREATED_AT, ]) token = data[self.__class__.__ENTITY_PROPERTY_TOKEN] customer_id = data[self.__class__.__ENTITY_PROPERTY_CUSTOMER_ID] if self.__elastic_cards.get_data(token): self.__elastic_cards.update_data( token, { 'doc': { 'is_verified': data[self.__class__.__ENTITY_PROPERTY_IS_VERIFIED] } }) else: self.__elastic_cards.create( token, { 'token': data[self.__class__.__ENTITY_PROPERTY_TOKEN], 'customer_id': data[self.__class__.__ENTITY_PROPERTY_CUSTOMER_ID], 'brand': data[self.__class__.__ENTITY_PROPERTY_BRAND], 'number_hidden': data[self.__class__.__ENTITY_PROPERTY_NUMBER_HIDDEN], 'expires': data[self.__class__.__ENTITY_PROPERTY_EXPIRES].strftime( '%y%m'), 'holder_name': data[self.__class__.__ENTITY_PROPERTY_HOLDER_NAME], 'is_verified': data[self.__class__.__ENTITY_PROPERTY_IS_VERIFIED], 'created_at': data[self.__class__.__ENTITY_PROPERTY_CREATED_AT].strftime( '%Y-%m-%d %H:%M:%S') }) # Elastic can search by attributes only after 1 second from last update. # We need all data, when we are searching by customer_id, # so in this case we will lost fresh data, if search directly after creation of new card. # In this case we need to use another index and get data by elastic doc_id. customer_cards_map = self.__elastic_customer_cards_map.get_data( customer_id) if customer_cards_map: tokens = json.loads(customer_cards_map['tokens_json']) tokens.append(token) self.__elastic_customer_cards_map.update_data( customer_id, {'doc': { 'tokens_json': json.dumps(tokens) }}) else: self.__elastic_customer_cards_map.create( customer_id, {'tokens_json': json.dumps([token])}) def remove(self, card: CreditCard) -> None: if not isinstance(card, CreditCard): raise ArgumentTypeException(self.remove, 'card', card) if not self.__elastic_cards.get_data(card.token): raise ArgumentValueException('Card #{} is already Removed!'.format( card.token)) self.__elastic_cards.delete_by_id(card.token) customer_cards_map = self.__elastic_customer_cards_map.get_data( card.customer_id) tokens = json.loads(customer_cards_map['tokens_json']) tokens = [token for token in tokens if token != card.token] self.__elastic_customer_cards_map.update_data( card.customer_id, {'doc': { 'tokens_json': json.dumps(tokens) }})
class _CancelRequestStorageElastic(CancelRequestStorageInterface): """ curl -X DELETE localhost:9200/purchase_cancel_requests curl -X PUT localhost:9200/purchase_cancel_requests -H "Content-Type: application/json" -d'{ "mappings": { "purchase_cancel_requests": { "properties": { "request_number": {"type": "keyword"}, "order_number": {"type": "keyword"}, "request_items": { "properties": { "simple_sku": {"type": "keyword"}, "qty": {"type": "integer"}, "status": {"type": "keyword"}, "processed_at": {"type": "date", "format": "date_hour_minute_second_millis"} } }, "refund_method": {"type": "keyword"}, "refund_method_extra_data_json": {"type": "keyword"}, "additional_comment": {"type": "keyword"}, "requested_at": {"type": "date", "format": "date_hour_minute_second_millis"} } } } }' curl -X DELETE localhost:9200/purchase_cancel_requests_orders_map curl -X PUT localhost:9200/purchase_cancel_requests_orders_map -H "Content-Type: application/json" -d'{ "mappings": { "purchase_cancel_requests_orders_map": { "properties": { "request_numbers_json": {"type": "keyword"} } } } }' """ __ENTITY_PROPERTY_REQUEST_NUMBER = '__number' __ENTITY_PROPERTY_ORDER_NUMBER = '__order_number' __ENTITY_PROPERTY_ITEMS = '__items' __ENTITY_PROPERTY_ITEMS_SIMPLE_SKU = '__simple_sku' __ENTITY_PROPERTY_ITEMS_QTY = '__qty' __ENTITY_PROPERTY_ITEMS_STATUS = '__status' __ENTITY_PROPERTY_ITEMS_PROCESSED_AT = '__processed_at' __ENTITY_PROPERTY_REFUND_METHOD = '__refund_method' __ENTITY_PROPERTY_ADDITIONAL_COMMENT = '__additional_comment' __ENTITY_PROPERTY_REQUESTED_AT = '__requested_at' def __init__(self): self.__requests_elastic = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_CANCEL_REQUESTS, settings.AWS_ELASTICSEARCH_PURCHASE_CANCEL_REQUESTS) self.__order_requests_map_elastic = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_CANCEL_REQUESTS_ORDERS_MAP, settings.AWS_ELASTICSEARCH_PURCHASE_CANCEL_REQUESTS_ORDERS_MAP) self.__reflector = Reflector() def save(self, cancel_request: CancelRequest) -> None: if not isinstance(cancel_request, CancelRequest): raise ArgumentTypeException(self.save, 'cancel_request', cancel_request) items_data = [] for item in cancel_request.items: items_data.append({ "simple_sku": item.simple_sku.value, "qty": item.qty.value, "status": item.status.value, # elastic supports only 3 digits for milliseconds "processed_at": item.processed_at.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] if item.processed_at else None }) document_id = cancel_request.number.value document_data = { "request_number": cancel_request.number.value, "order_number": cancel_request.order_number.value, "request_items": items_data, 'refund_method': cancel_request.refund_method.descriptor, 'refund_method_extra_data_json': json.dumps(cancel_request.refund_method.extra_data), # elastic supports only 3 digits for milliseconds "requested_at": cancel_request.requested_at.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3], "additional_comment": cancel_request.additional_comment.value if cancel_request.additional_comment else None } existed_request = self.get_by_number(cancel_request.number) if existed_request: self.__requests_elastic.update_data(document_id, {'doc': document_data}) else: self.__requests_elastic.create(document_id, document_data) # Elastic can search by attributes only after 1 second from last update. # We need all data, when we are searching by order_number, # so in this case we will lost fresh data, if search directly after creation of a new cancel_request. # In this case we need to use another index and get data by elastic doc_id. order_requests_map = self.__order_requests_map_elastic.get_data( cancel_request.order_number.value) if order_requests_map: request_numbers = list( json.loads( order_requests_map.get('request_numbers_json', '[]')) or []) request_numbers.append(cancel_request.number.value) request_numbers = list(set(request_numbers)) self.__order_requests_map_elastic.update_data( cancel_request.order_number.value, { 'doc': { 'request_numbers_json': json.dumps(request_numbers) } }) else: self.__order_requests_map_elastic.create( cancel_request.order_number.value, { 'request_numbers_json': json.dumps([cancel_request.number.value]) }) def get_by_number( self, request_number: CancelRequest.Number) -> Optional[CancelRequest]: if not isinstance(request_number, CancelRequest.Number): raise ArgumentTypeException(self.get_by_number, 'request_number', request_number) data = self.__requests_elastic.get_data(request_number.value) result = self.__restore(data) if data else None return result def __restore(self, data: dict) -> CancelRequest: cancel_request = self.__reflector.construct( CancelRequest, { self.__class__.__ENTITY_PROPERTY_REQUEST_NUMBER: CancelRequest.Number(data['request_number']), self.__class__.__ENTITY_PROPERTY_ORDER_NUMBER: OrderNumber(data['order_number']), self.__class__.__ENTITY_PROPERTY_ITEMS: tuple([ self.__reflector.construct( CancelRequest.Item, { self.__class__.__ENTITY_PROPERTY_ITEMS_SIMPLE_SKU: SimpleSku(item_data['simple_sku']), self.__class__.__ENTITY_PROPERTY_ITEMS_QTY: Qty(item_data['qty']), self.__class__.__ENTITY_PROPERTY_ITEMS_STATUS: CancelRequest.Item.Status(item_data['status']), self.__class__.__ENTITY_PROPERTY_ITEMS_PROCESSED_AT: (datetime.datetime.strptime( item_data['processed_at'] + '000', '%Y-%m-%dT%H:%M:%S.%f') if item_data['processed_at'] else None), }) for item_data in data['request_items'] ]), self.__class__.__ENTITY_PROPERTY_REFUND_METHOD: _restore_refund_method( data['refund_method'], json.loads(data['refund_method_extra_data_json'])), self.__class__.__ENTITY_PROPERTY_ADDITIONAL_COMMENT: (CancelRequest.AdditionalComment(data['additional_comment']) if data.get('additional_comment') or None else None), self.__class__.__ENTITY_PROPERTY_REQUESTED_AT: datetime.datetime.strptime(data['requested_at'] + '000', '%Y-%m-%dT%H:%M:%S.%f'), }) return cancel_request def get_all_by_order_number( self, order_number: OrderNumber) -> Tuple[CancelRequest]: if not isinstance(order_number, OrderNumber): raise ArgumentTypeException(self.get_all_by_order_number, 'order_number', order_number) data = self.__order_requests_map_elastic.get_data(order_number.value) request_numbers = json.loads(( data.get('request_numbers_json') or '[]') if data else '[]') or [] if not request_numbers: return tuple() rows = self.__requests_elastic.post_search({ "query": { "ids": { "values": request_numbers } }, "size": 10000 }).get('hits', {}).get('hits', []) or [] result = [self.__restore(row['_source']) for row in rows] if len(result) != len(request_numbers): message = '{} can\'t find all CancelRequests for Order #{}! Not existed CancelRequests in map: {}' raise ValueError( message.format( self.get_all_by_order_number, order_number.value, [ request_number for request_number in request_numbers if request_number not in [request.number.value for request in result] ])) return tuple(result)
class _OrderElasticStorage(OrderStorageInterface): """ curl -X DELETE localhost:9200/purchase_orders curl -X PUT localhost:9200/purchase_orders -H "Content-Type: application/json" -d'{ "mappings": { "purchase_orders": { "properties": { "order_number": {"type": "keyword"}, "customer_id": {"type": "keyword"}, "order_items": { "properties": { "event_code": {"type": "keyword"}, "simple_sku": {"type": "keyword"}, "product_original_price": {"type": "float"}, "product_current_price": {"type": "float"}, "dtd_occasion_name": {"type": "keyword"}, "dtd_occasion_description": {"type": "keyword"}, "dtd_date_from": {"type": "date", "format": "date"}, "dtd_date_to": {"type": "date", "format": "date"}, "dtd_min": {"type": "integer"}, "dtd_max": {"type": "integer"}, "qty_ordered": {"type": "integer"}, "qty_return_requested": {"type": "integer"}, "qty_return_returned": {"type": "integer"}, "qty_cancelled_before_payment": {"type": "integer"}, "qty_cancelled_after_payment_requested": {"type": "integer"}, "qty_cancelled_after_payment_cancelled": {"type": "integer"}, "qty_refunded": {"type": "integer"}, "qty_modified_at": {"type": "date", "format": "date_hour_minute_second_millis"}, "fbucks_amount": {"type": "float"} } }, "delivery_address_recipient_name": {"type": "keyword"}, "delivery_address_phone_number": {"type": "keyword"}, "delivery_address_street_address": {"type": "keyword"}, "delivery_address_suburb": {"type": "keyword"}, "delivery_address_city": {"type": "keyword"}, "delivery_address_province": {"type": "keyword"}, "delivery_address_complex_building": {"type": "keyword"}, "delivery_address_postal_code": {"type": "keyword"}, "delivery_address_business_name": {"type": "keyword"}, "delivery_address_special_instructions": {"type": "keyword"}, "delivery_cost": {"type": "float"}, "vat_percent": {"type": "float"}, "credits_spent": {"type": "float"}, "payment_method": {"type": "keyword"}, "payment_method_extra_data_json": {"type": "keyword"}, "status_history": { "properties": { "status": {"type": "keyword"}, "datetime": {"type": "date", "format": "date_hour_minute_second_millis"} } } } } } }' curl -X DELETE localhost:9200/purchase_orders_customer_orders_map curl -X PUT localhost:9200/purchase_orders_customer_orders_map -H "Content-Type: application/json" -d'{ "mappings": { "purchase_orders_customer_orders_map": { "properties": { "order_numbers_json": {"type": "keyword"} } } } }' """ def __init__(self): self.__orders_elastic = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_ORDERS, settings.AWS_ELASTICSEARCH_PURCHASE_ORDERS) self.__customer_orders_map_elastic = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_ORDERS_CUSTOMER_ORDERS_MAP, settings.AWS_ELASTICSEARCH_PURCHASE_ORDERS_CUSTOMER_ORDERS_MAP) self.__reflector = Reflector() self.__current_vat_value = PurchaseSettings().vat def save(self, order: Order) -> None: if not isinstance(order, Order): raise ArgumentTypeException(self.save, 'order', order) order_number = order.number delivery_address = order.delivery_address status_changes = order.status_history document_id = order_number.value document_data = { 'order_number': order_number.value, 'customer_id': order.customer_id.value, 'order_items': [ { 'event_code': item.event_code.value, 'simple_sku': item.simple_sku.value, 'product_original_price': item.product_original_price.value, 'product_current_price': item.product_current_price.value, 'dtd_occasion_name': item.dtd.occasion.name.value if item.dtd.occasion else None, 'dtd_occasion_description': item.dtd.occasion.description.value if item.dtd.occasion else None, 'dtd_date_from': item.dtd.date_from.strftime('%Y-%m-%d'), 'dtd_date_to': item.dtd.date_to.strftime('%Y-%m-%d'), 'dtd_working_days_from': item.dtd.working_days_from, 'dtd_working_days_to': item.dtd.working_days_to, 'qty_ordered': item.qty_ordered.value, 'qty_return_requested': item.qty_return_requested.value, 'qty_return_returned': item.qty_return_returned.value, 'qty_cancelled_before_payment': item.qty_cancelled_before_payment.value, 'qty_cancelled_after_payment_requested': item.qty_cancelled_after_payment_requested.value, 'qty_cancelled_after_payment_cancelled': item.qty_cancelled_after_payment_cancelled.value, 'qty_refunded': item.qty_refunded.value, # elastic supports only 3 digits for milliseconds 'qty_modified_at': item.qty_modified_at.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3], 'fbucks_amount': item.fbucks_earnings.value, } for item in order.items ], 'delivery_address_recipient_name': delivery_address.recipient_name, 'delivery_address_phone_number': delivery_address.phone_number, 'delivery_address_street_address': delivery_address.street_address, 'delivery_address_suburb': delivery_address.suburb, 'delivery_address_city': delivery_address.city, 'delivery_address_province': delivery_address.province, 'delivery_address_complex_building': delivery_address.complex_building, 'delivery_address_postal_code': delivery_address.postal_code, 'delivery_address_business_name': delivery_address.business_name, 'delivery_address_special_instructions': delivery_address.special_instructions, 'delivery_cost': order.delivery_cost.value, 'vat_percent': order.vat_percent.value, 'credits_spent': order.credit_spent_amount.value, 'payment_method': order.payment_method.descriptor if order.payment_method else None, 'payment_method_extra_data_json': json.dumps(order.payment_method.extra_data if order. payment_method else {}), 'status_history': [ { 'status': status_change.status.value, # elastic supports only 3 digits for milliseconds 'datetime': status_change.datetime.strftime('%Y-%m-%dT%H:%M:%S.%f') [:-3], } for status_change in status_changes ], } existed_order = self.load(order_number) if existed_order: # just a double check of order number uniqueness if existed_order.customer_id != order.customer_id: raise RuntimeError( 'Order "{}" already exists and belongs to another Customer!' .format(order_number)) self.__orders_elastic.update_data(document_id, {'doc': document_data}) else: self.__orders_elastic.create(document_id, document_data) # Elastic can search by attributes only after 1 second from last update. # We need all data, when we are searching by customer_id, # so in this case we will lost fresh data, if search directly after creation of new order. # In this case we need to use another index and get data by elastic doc_id. customer_orders_map = self.__customer_orders_map_elastic.get_data( order.customer_id.value) if customer_orders_map: order_numbers = list( json.loads( customer_orders_map.get('order_numbers_json', '[]')) or []) order_numbers.append(order.number.value) order_numbers = list(set(order_numbers)) self.__customer_orders_map_elastic.update_data( order.customer_id.value, {'doc': { 'order_numbers_json': json.dumps(order_numbers) }}) else: self.__customer_orders_map_elastic.create( order.customer_id.value, {'order_numbers_json': json.dumps([order.number.value])}) def load(self, order_number: Order.Number) -> Optional[Order]: if not isinstance(order_number, Order.Number): raise ArgumentTypeException(self.load, 'order_number', order_number) data = self.__orders_elastic.get_data(order_number.value) result = self.__restore(data) if data else None return result def __restore(self, data: dict) -> Order: order_number = Order.Number(data.get('order_number')) customer_id = Id(data.get('customer_id')) delivery_cost = Cost(float(data.get('delivery_cost'))) vat_percent = Percentage( float( # I added "vat_percent" after first orders were stored, # but it's hard to make changes in elastic, so... # @todo : create migration tool. data.get('vat_percent') or self.__current_vat_value)) credits_spent = Cost(float(data.get('credits_spent') or '0')) # can be not existed in old data payment_method = self.__restore_payment_method( data.get('payment_method'), json.loads(data.get('payment_method_extra_data_json') or '{}') if data.get('payment_method') else None) delivery_address = DeliveryAddress( data.get('delivery_address_recipient_name'), data.get('delivery_address_phone_number'), data.get('delivery_address_street_address'), data.get('delivery_address_suburb'), data.get('delivery_address_city'), data.get('delivery_address_province'), data.get('delivery_address_complex_building'), data.get('delivery_address_postal_code'), data.get('delivery_address_business_name'), data.get('delivery_address_special_instructions')) status_changes = [] for status_change_data in data.get('status_history'): status = Order.Status(status_change_data.get('status')) # elastic supports only 3 digits for milliseconds changed_at = datetime.datetime.strptime( status_change_data.get('datetime') + '000', '%Y-%m-%dT%H:%M:%S.%f') status_change = self.__reflector.construct( Order.StatusChangesHistory.Change, { '__status': status, '__datetime': changed_at }) status_changes.append(status_change) status_change_history = Order.StatusChangesHistory( tuple(status_changes)) order_items = [] for item_data in data.get('order_items'): event_code = EventCode(item_data.get('event_code')) simple_sku = SimpleSku(item_data.get('simple_sku')) product_original_price = Cost( item_data.get('product_original_price')) product_current_price = Cost( item_data.get('product_current_price')) fbucks_earnings = Cost(item_data.get('fbucks_amount') or 0) # old orders don't have this field dtd = Dtd( Dtd.Occasion( Name(item_data.get('dtd_occasion_name')), Description(item_data.get('dtd_occasion_description'))) if item_data.get('dtd_occasion_name') else None, datetime.date( int(item_data.get('dtd_date_from').split('-')[0]), int(item_data.get('dtd_date_from').split('-')[1]), int(item_data.get('dtd_date_from').split('-')[2])), datetime.date(int(item_data.get('dtd_date_to').split('-')[0]), int(item_data.get('dtd_date_to').split('-')[1]), int(item_data.get('dtd_date_to').split('-')[2])), int(item_data.get('dtd_working_days_from')), int(item_data.get('dtd_working_days_to'))) qty_ordered = Qty(int(item_data.get('qty_ordered'))) qty_return_requested = Qty( int(item_data.get('qty_return_requested') or 0)) qty_return_returned = Qty( int(item_data.get('qty_return_returned') or 0)) qty_cancelled_before_payment = Qty( int(item_data.get('qty_cancelled_before_payment') or 0)) qty_cancelled_after_payment_requested = Qty( int( item_data.get('qty_cancelled_after_payment_requested') or 0)) qty_cancelled_after_payment_cancelled = Qty( int( item_data.get('qty_cancelled_after_payment_cancelled') or 0)) qty_refunded = Qty(int(item_data.get('qty_refunded') or 0)) # elastic supports only 3 digits for milliseconds qty_modified_at = datetime.datetime.strptime( ( # "qty_modified_at" may not exist for old data (dev, test), # but it's hard to make changes in elastic, so... # @todo : create migration tool. item_data.get('qty_modified_at') or status_change_history.get_last().datetime.strftime( '%Y-%m-%dT%H:%M:%S.%f')[:-3]) + '000', '%Y-%m-%dT%H:%M:%S.%f') order_item = self.__reflector.construct( Order.Item, { '__event_code': event_code, '__simple_sku': simple_sku, '__product_original_price': product_original_price, '__product_current_price': product_current_price, '__dtd': dtd, '__qty_ordered': qty_ordered, '__qty_return_requested': qty_return_requested, '__qty_return_returned': qty_return_returned, '__qty_cancelled_before_payment': qty_cancelled_before_payment, '__qty_cancelled_after_payment_requested': qty_cancelled_after_payment_requested, '__qty_cancelled_after_payment_cancelled': qty_cancelled_after_payment_cancelled, '__qty_refunded': qty_refunded, '__qty_modified_at': qty_modified_at, '__fbucks_earnings': fbucks_earnings }) order_items.append(order_item) order = self.__reflector.construct( Order, { '__order_number': order_number, '__customer_id': customer_id, '__items': order_items, '__delivery_address': delivery_address, '__delivery_cost': delivery_cost, '__vat_percent': vat_percent, '__payment_method': payment_method, '__status_history': status_change_history, '__credits_spent': credits_spent, }) return order def __restore_payment_method( self, descriptor: Optional[str], extra_data: Optional[dict], ) -> Optional[Order.PaymentMethodAbstract]: if not descriptor: return None # @todo : refactoring !!! if descriptor == 'regular_eft': return RegularEftOrderPaymentMethod() elif descriptor == 'mobicred': return MobicredPaymentMethod(extra_data['payment_id']) elif descriptor == 'credit_card': return CreditCardOrderPaymentMethod(extra_data['payment_id']) elif descriptor == 'customer_credit': return CustomerCreditsOrderPaymentMethod() raise Exception( '{} does not know, how to restore {} payment method with data {}!'. format(self.__restore_payment_method, descriptor, extra_data)) def get_all_by_numbers(self, order_numbers: Tuple[Order.Number]) -> Tuple[Order]: if sum([ not isinstance(order_number, Order.Number) for order_number in order_numbers ]) > 0: raise ArgumentTypeException(self.get_all_by_numbers, 'order_numbers', order_numbers) rows = self.__orders_elastic.post_search({ "query": { "ids": { "values": [order_number.value for order_number in order_numbers] } }, "size": 10000 }).get('hits', {}).get('hits', []) or [] result = [self.__restore(row['_source']) for row in rows] return tuple(result) def get_all_for_customer(self, customer_id: Id) -> Tuple[Order]: if not isinstance(customer_id, Id): raise ArgumentTypeException(self.get_all_for_customer, 'customer_id', customer_id) data = self.__customer_orders_map_elastic.get_data(customer_id.value) order_numbers = json.loads(( data.get('order_numbers_json') or '[]') if data else '[]') or [] if not order_numbers: return tuple() rows = self.__orders_elastic.post_search({ "query": { "ids": { "values": order_numbers } }, "size": 10000 }).get('hits', {}).get('hits', []) or [] result = [self.__restore(row['_source']) for row in rows] if len(result) != len(order_numbers): message = '{} can\'t find all Orders for Customer #{}! Not existed order in map: {}' raise ValueError( message.format(self.get_all_for_customer, customer_id.value, [ order_number for order_number in order_numbers if order_number not in [order.number.value for order in result] ])) return tuple(result)
class Product(object): # @todo : refactoring ~ get_all(criteria, limit, offset) def __init__(self): self.__elastic = Elastic(settings.AWS_ELASTICSEARCH_PRODUCTS, settings.AWS_ELASTICSEARCH_PRODUCTS) def get_all(self, convert: bool = False): # TODO: Should be refactored later. offset, CHUNK_SIZE = 0, 1000 products = [] query = { "size": CHUNK_SIZE, "from": offset, } response = self.__elastic.post_search(query)['hits'] total = response['total'] products += response['hits'] while len(products) < total: offset += CHUNK_SIZE query = { "size": CHUNK_SIZE, "from": offset, } response = self.__elastic.post_search(query)['hits'] products += response['hits'] if convert: return self.__convert_products({'total': total, 'hits': products}) else: return [ProductEntry(**item['_source']) for item in products] def listAll(self, sort, order, page=1, size=18): fromindex = (int(page) - 1) * int(size) if fromindex < 0: fromindex = 0 sort = self.__class__.__convert_filter(sort) if sort == "invalid_name": return {"error": "invalid sort field"} query = { "size": size, "from": fromindex, "sort": [{ sort: { "order": order } }] } response = self.__elastic.post_search(query)['hits'] return self.__convert_products(response) def __makeESFilterFromCustomFilter(self, custom_filters: Optional[dict]): ret = {} ret['bool'] = {} ret['bool']['must'] = list() for key, value in custom_filters.items() if custom_filters else []: key = self.__class__.__convert_filter(key) if (key == "invalid_name"): continue must_item = {} if key == 'rs_selling_price': must_item['range'] = {} must_item['range'][key] = {} must_item['range'][key]['gte'] = value[0] must_item['range'][key]['lte'] = value[1] elif key == 'search_query': value = str(value or '').strip() if value: must_item['bool'] = { "should": [ { "match_phrase_prefix": { "product_name": value } }, # {"match_phrase_prefix": {"product_description": value}} When search_query is 'Dress', this returns some socks and shoes { "match_phrase_prefix": { "product_size_attribute": value } } ] } elif key == 'created_at': if value == 'true': from_date = (datetime.now() - timedelta(days=settings.NEW_PRODUCT_THRESHOLD) ).strftime("%Y-%m-%d %H:%M:%S") must_item['range'] = {} must_item['range'][key] = {} must_item['range'][key]['gte'] = from_date else: continue else: if isinstance(value, list): must_item['bool'] = {} must_item['bool']['should'] = [] for _value in value: match = {} match['match'] = {} match['match'][key] = _value must_item['bool']['should'].append(match) else: must_item['match'] = {} must_item['match'][key] = value if must_item: ret['bool']['must'].append(must_item) return ret def listByCustomFilter(self, custom_filters: Optional[dict], sorts: dict, tier: dict, page: int, size: int): filters = self.__makeESFilterFromCustomFilter(custom_filters) fromindex = (int(page) - 1) * int(size) if fromindex < 0: fromindex = 0 query = { "query": filters, "size": size, "from": fromindex, "sort": [ self.__class__.__convert_sort_filter(column, direction) for column, direction in sorts.items() ], } response = self.__elastic.post_search(query)['hits'] return self.__convert_products(response, tier=tier) def update(self, config_sku, data): json_data = {"doc": data} response = self.__elastic.update_data(config_sku, json_data) return response def get(self, id, log: bool = False, session_id: str = None, customer_id: str = None): item = self.__elastic.get_data(id) if log and item is not None: # TODO: refactoring - move out from model's method log_model = ProductVisitLog(session_id, customer_id=customer_id) log_model.insert(self.__convert_item(item)) # Fix "An AttributeValue may not contain an empty string" error item['size_chart'] = item.get('size_chart') or None item['img'] = { 'media_gallery': item.get('img', {}).get('media_gallery', []), 'images': { 'lifestyle': item.get('img', {}).get('images', {}).get('lifestyle') or None, 'small': item.get('img', {}).get('images', {}).get('small') or None, 'back': item.get('img', {}).get('images', {}).get('back') or None, } } return item def __convert_products(self, data, tier: dict = None): ret = { "total": data["total"], "products": [ self.__convert_item(item["_source"], tier=tier) for item in data["hits"] ] } return ret @staticmethod def __convert_item_calculate_prices(item) -> tuple: original_price = float(item['rs_selling_price'] or 0) discount = float(item['discount'] or 0) current_price = original_price - original_price * discount / 100 return original_price, current_price def __convert_item(self, item, tier: dict = None): original_price, current_price = self.__class__.__convert_item_calculate_prices( item) fbucks = None if isinstance(tier, dict) and not tier.get('is_neutral'): fbucks = math.ceil( item.get('current_price', current_price) * tier['discount_rate'] / 100) result = { 'id': item['portal_config_id'], 'sku': item['rs_sku'], 'event_code': item['event_code'], 'title': item['product_name'], 'subtitle': item['product_description'], 'price': item['rs_selling_price'], 'discount': item['discount'], 'original_price': original_price, 'current_price': current_price, 'fbucks': fbucks, # 'badge': 'NEW IN' if datetime.strptime(item['created_at'], "%Y-%m-%d %H:%M:%S") > from_date else None, 'product_type': item['product_size_attribute'], 'product_sub_type': item['rs_product_sub_type'], 'gender': item['gender'], 'brand': item['manufacturer'], 'color': item['rs_colour'], 'sizes': [{ 'size': size['size'], 'qty': size['qty'], 'simple_sku': size['rs_simple_sku'], 'simple_id': size['portal_simple_id'], } for size in item.get('sizes', [])], 'image': { 'src': item['images'][0]['s3_filepath'] if len(item['images']) > 0 else 'https://www.supplyforce.com/ASSETS/WEB_THEMES//ECOMMERCE_STD_TEMPLATE_V2/images/NoImage.png', 'title': item['product_size_attribute'], } } return result @staticmethod def __convert_filter(filter_name): switcher = { 'id': 'portal_config_id', 'sku': 'rs_sku', 'title': 'product_name', 'subtitle': 'product_description', 'price': 'rs_selling_price', 'product_type': 'product_size_attribute', 'product_sub_type': 'rs_product_sub_type', 'gender': 'gender', 'brand': 'manufacturer', 'size': 'sizes.size', 'color': 'rs_colour', # 'newin': 'created_at', '_score': '_score', 'search_query': 'search_query' } return switcher.get(filter_name, "invalid_name") @staticmethod def __convert_sort_filter(column_name, direction): sort_map = { 'id': { 'portal_config_id': { 'order': direction } }, 'sku': { 'rs_sku': { 'order': direction } }, 'title': { 'product_name': { 'order': direction } }, 'subtitle': { 'product_description': { 'order': direction } }, 'product_type': { 'product_size_attribute': { 'order': direction } }, 'product_sub_type': { 'rs_product_sub_type': { 'order': direction } }, 'gender': { 'gender': { 'order': direction } }, 'brand': { 'manufacturer': { 'order': direction } }, 'size': { 'sizes.size': { 'order': direction } }, 'color': { 'rs_colour': { 'order': direction } }, 'newin': { 'created_at': { 'order': direction } }, '_score': { '_score': { 'order': direction } }, 'search_query': { 'search_query': { 'order': direction } }, 'price': { '_script': { "type": "number", "script": { "lang": "painless", # see __convert_item_calculate_prices() "source": "{price} - {price} * {discount} / 100".format( **{ 'price': "doc['rs_selling_price'].value", 'discount': "doc['discount'].value" }), }, "order": direction } } } if column_name not in sort_map.keys(): raise ValueError( 'Oh, no! {} does not know, how to {} with {} column!'.format( Product.__qualname__, '__convert_sort_filter', column_name)) return sort_map[column_name] def __getSumofQTY(self, data): filters = self.__makeESFilterFromCustomFilter(data) query = { "query": filters, "size": 0, "aggregations": { "count": { "sum": { "field": "sizes.qty" } } } } aggregations = self.__elastic.post_search(query)['aggregations'] return aggregations['count']['value'] def __getAvailableSubType(self, data, sort): filters = self.__makeESFilterFromCustomFilter(data) query = { "query": filters, "size": 0, "aggregations": { "product_sub_type": { "terms": { "field": "rs_product_sub_type", "size": 100000 } } } } aggregations = self.__elastic.post_search(query)['aggregations'] sub_types = [] for item in aggregations['product_sub_type']['buckets']: sub_types.append(item['key']) if sort == 'asc': sub_types.sort() elif sort == 'desc': sub_types.sort(reverse=True) ret = [] for sub_type in sub_types: new_filter = data.copy() new_filter['product_sub_type'] = sub_type ret.append({ 'label': sub_type, # 'count': self.__getSumofQTY(new_filter) }) return ret # This is too slow. def getNewAvailableFilter(self, data, sort): filters = self.__makeESFilterFromCustomFilter(data) query = { "query": filters, "size": 0, "aggregations": { "gender": { "terms": { "field": "gender", "size": 100000 } }, "price": { "terms": { "field": "rs_selling_price", "size": 100000 } }, "product_type": { "terms": { "field": "product_size_attribute", "size": 100000 } }, "brand": { "terms": { "field": "manufacturer", "size": 100000 } }, "size": { "terms": { "field": "sizes.size", "size": 100000 } }, "color": { "terms": { "field": "rs_colour", "size": 100000 } } } } aggregations = self.__elastic.post_search(query)['aggregations'] availablefilter = { 'product_type': [], 'brand': [], 'size': [], 'color': [], 'price': [], 'gender': [] } for item in aggregations['product_type']['buckets']: product_type = {} product_type['label'] = item['key'] new_filter = data.copy() new_filter['product_type'] = item['key'] product_type['children'] = self.__getAvailableSubType( new_filter, sort) # product_type['count']=self.__getSumofQTY(new_filter) availablefilter['product_type'].append(product_type) for item in aggregations['brand']['buckets']: brand = {} brand['label'] = item['key'] new_filter = data.copy() new_filter['brand'] = item['key'] # brand['count']=self.__getSumofQTY(new_filter) availablefilter['brand'].append(brand) for item in aggregations['color']['buckets']: color = {} color['label'] = item['key'] new_filter = data.copy() new_filter['color'] = item['key'] # color['count']=self.__getSumofQTY(new_filter) availablefilter['color'].append(color) for item in aggregations['size']['buckets']: size = {} size['label'] = item['key'] availablefilter['size'].append(size) for item in aggregations['price']['buckets']: availablefilter['price'].append(item['key']) for item in aggregations['gender']['buckets']: gender = {} gender['label'] = item['key'] new_filter = data.copy() new_filter['gender'] = item['key'] # gender['count']=self.__getSumofQTY(new_filter) availablefilter['gender'].append(gender) for key, items in availablefilter.items(): if key == 'size': _sizes = [item['label'] for item in items] _sorted_sizes = ProductSizeSort().sort(_sizes) if sort == 'desc': _sorted_sizes.reverse() availablefilter['size'].clear() for _item in _sorted_sizes: size = {} size['label'] = _item new_filter = data.copy() new_filter['size'] = _item # size['count']=self.__getSumofQTY(new_filter) availablefilter['size'].append(size) elif key == 'price': price_list = { 'Under R100': 0, 'R100 - R250': 0, 'R250 - R500': 0, 'R500 - R750': 0, 'R750 - R1,000': 0, 'R1,000 - R2,000': 0, 'Over R2,000': 0 } for _item in items: if _item < 100: price_list['Under R100'] += 1 continue elif _item < 250: price_list['R100 - R250'] += 1 continue elif _item < 500: price_list['R250 - R500'] += 1 continue elif _item < 750: price_list['R500 - R750'] += 1 continue elif _item < 1000: price_list['R750 - R1,000'] += 1 continue elif _item < 2000: price_list['R1,000 - R2,000'] += 1 continue else: price_list['R1,000 - R2,000'] += 1 availablefilter['price'].clear() if sort == 'asc': if price_list['Under R100'] > 0: availablefilter['price'].append({ 'label': 'Under R100', 'value': [0, 100] }) if price_list['R100 - R250'] > 0: availablefilter['price'].append({ 'label': 'R100 - R250', 'value': [100, 250] }) if price_list['R250 - R500'] > 0: availablefilter['price'].append({ 'label': 'R250 - R500', 'value': [250, 500] }) if price_list['R500 - R750'] > 0: availablefilter['price'].append({ 'label': 'R500 - R750', 'value': [500, 750] }) if price_list['R750 - R1,000'] > 0: availablefilter['price'].append({ 'label': 'R750 - R1,000', 'value': [750, 1000] }) if price_list['R1,000 - R2,000'] > 0: availablefilter['price'].append({ 'label': 'R1,000 - R2,000', 'value': [1000, 2000] }) if price_list['Over R2,000'] > 0: availablefilter['price'].append({ 'label': 'Over R2,000', 'value': [2000, 10000] }) elif sort == 'desc': if price_list['Over R2,000'] > 0: availablefilter['price'].append({ 'label': 'Over R2,000', 'value': [2000, 10000] }) if price_list['R1,000 - R2,000'] > 0: availablefilter['price'].append({ 'label': 'R1,000 - R2,000', 'value': [1000, 2000] }) if price_list['R750 - R1,000'] > 0: availablefilter['price'].append({ 'label': 'R750 - R1,000', 'value': [750, 1000] }) if price_list['R500 - R750'] > 0: availablefilter['price'].append({ 'label': 'R500 - R750', 'value': [500, 750] }) if price_list['R250 - R500'] > 0: availablefilter['price'].append({ 'label': 'R250 - R500', 'value': [250, 500] }) if price_list['R100 - R250'] > 0: availablefilter['price'].append({ 'label': 'R100 - R250', 'value': [100, 250] }) if price_list['Under R100'] > 0: availablefilter['price'].append({ 'label': 'Under R100', 'value': [0, 100] }) else: if sort == 'asc': availablefilter[key] = sorted( items, key=lambda i: i['label'].lower()) elif sort == 'desc': availablefilter[key] = sorted( items, key=lambda i: i['label'].lower(), reverse=True) return availablefilter def getAvailableFilter(self, data, sort): filters = self.__makeESFilterFromCustomFilter(data) query = { "query": filters, "size": 0, "aggregations": { "gender": { "terms": { "field": "gender", "size": 100000 } }, "price": { "terms": { "field": "rs_selling_price", "size": 100000 } }, "product_type": { "terms": { "field": "product_size_attribute", "size": 100000 } }, "product_sub_type": { "terms": { "field": "rs_product_sub_type", "size": 100000 } }, "brand": { "terms": { "field": "manufacturer", "size": 100000 } }, "size": { "terms": { "field": "sizes.size", "size": 100000 } }, "color": { "terms": { "field": "rs_colour", "size": 100000 } } } } aggregations = self.__elastic.post_search(query)['aggregations'] availablefilter = { 'product_type': [], 'product_sub_type': [], 'brand': [], 'size': [], 'color': [], 'price': [], 'gender': [] } for item in aggregations['product_type']['buckets']: availablefilter['product_type'].append(item['key']) for item in aggregations['product_sub_type']['buckets']: availablefilter['product_sub_type'].append(item['key']) for item in aggregations['brand']['buckets']: availablefilter['brand'].append(item['key']) for item in aggregations['color']['buckets']: availablefilter['color'].append(item['key']) for item in aggregations['size']['buckets']: availablefilter['size'].append(item['key']) for item in aggregations['price']['buckets']: availablefilter['price'].append(item['key']) for item in aggregations['gender']['buckets']: availablefilter['gender'].append(item['key']) for key, items in availablefilter.items(): if key == 'size': sorted_sizes = ProductSizeSort().sort(items) if sort == 'desc': sorted_sizes.reverse() availablefilter[key] = sorted_sizes else: if sort == 'asc': items.sort() elif sort == 'desc': items.sort(reverse=True) return availablefilter def updateStock(self, items): try: total_found_item_count = 0 updated_item_count = 0 added_item_count = 0 for item in items: script = ( "for(int j = 0; j < ctx._source.sizes.size(); j++) if(ctx._source.sizes[j].rs_simple_sku == '" + item['rs_simple_sku'] + "'){ ctx._source.sizes[j].qty = " + str(item['qty']) + "; break; }") query = { "script": script, "query": { "bool": { "must": [{ "match": { "sizes.portal_simple_id": item['product_simple_id'] } }, { "match": { "sizes.rs_simple_sku": item['rs_simple_sku'] } }] } } } res = self.__elastic.update_by_query(query) total_found_item_count += res['total'] updated_item_count += res['updated'] if res['total'] == 0: sku_size = item['rs_simple_sku'].split('-') rs_sku = sku_size[0] size = sku_size[1] product = self.get(rs_sku) if product is not None: _inline = "ctx._source.sizes.add(params.size)" _size = { "size": size, "portal_simple_id": item['product_simple_id'], "qty": item['qty'], "rs_simple_sku": item['rs_simple_sku'], } _query = { "script": { "lang": "painless", "inline": _inline, "params": { "size": _size } } } res = self.__elastic.update_data(rs_sku, _query) if res['_id'] == rs_sku: added_item_count += 1 return { 'total_found_item': total_found_item_count, 'updated_item': updated_item_count, 'added_item': added_item_count } except: return {'result': 'failure'} def getRawDataBySimpleSkus(self, simple_skus: Union[Tuple[str], List[str]], convert=True) -> Tuple[dict]: response_items = self.__elastic.post_search({ 'query': { 'bool': { 'filter': { 'terms': { 'sizes.rs_simple_sku': simple_skus } } } }, 'size': 10000, }).get('hits', {}).get('hits', []) or [] result = [ self.__convert_item(data['_source']) if convert else data['_source'] for data in response_items ] return tuple(result) def getRawDataBySimpleSku(self, simple_sku: str, convert=True) -> Optional[dict]: rows = self.getRawDataBySimpleSkus([simple_sku], convert) return rows[0] if rows else None def get_raw_data(self, config_sku: str, convert=False) -> Optional[dict]: try: product_data = self.__elastic.post_search({ 'query': { 'term': { 'rs_sku': config_sku } } }).get('hits', {}).get('hits', [{}])[0].get('_source') result = self.__convert_item( product_data) if convert else product_data return result except: return None def get_raw_data_by_skus(self, config_skus: List[str], convert=False) -> List[dict]: try: response = self.__elastic.post_search({ 'query': { 'terms': { 'rs_sku': config_skus } } }).get('hits', {}).get('hits', [{}]) if convert: return [ self.__convert_item(item['_source']) for item in response ] else: return [item['_source'] for item in response] except: return []
def __return(cart_items: Tuple[Cart.Item], original_subtotal: float, current_subtotal: float, current_subtotal_vat_amount: float): tier = blueprint.current_request.current_user.profile.tier # fbucks available to spend available_fbucks_amount = None if not tier[ 'is_neutral'] and not blueprint.current_request.current_user.is_anyonimous: """""" # @TODO : REFACTORING !!! from chalicelib.libs.purchase.customer.sqs import FbucksChargeSqsHandler see = FbucksChargeSqsHandler """""" from chalicelib.settings import settings from chalicelib.libs.core.elastic import Elastic __fbucks_customer_amount_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT, ) fbucks_amount_row = __fbucks_customer_amount_elastic.get_data( blueprint.current_request.current_user.id) available_fbucks_amount = fbucks_amount_row[ 'amount'] or 0 if fbucks_amount_row else 0 products = MpcProduct() items_data = [] for cart_item in cart_items: product = products.getRawDataBySimpleSku( cart_item.simple_sku.value) product_sizes = product.get('sizes', []) if product else () size = tuple( filter( lambda s: s.get('simple_sku') == cart_item.simple_sku. value, product_sizes))[0] dtd = dtd_calculator.calculate(cart_item.simple_sku, cart_item.qty) item_fbucks = None if not tier[ 'is_neutral'] and not blueprint.current_request.current_user.is_anyonimous: item_fbucks = math.ceil(cart_item.current_cost.value * tier['discount_rate'] / 100) items_data.append({ 'sku': product['sku'], 'simple_sku': cart_item.simple_sku.value, 'name': product.get('title'), 'brand_name': product.get('brand'), 'size_name': size.get('size'), 'image_url': product.get('image', {}).get('src', None), 'qty_available': int(size.get('qty')), 'qty_added': cart_item.qty.value, 'is_added_over_limit': cart_item.is_added_over_limit, 'product_original_price': cart_item.product_original_price.value, 'product_current_price': cart_item.product_current_price.value, 'original_cost': cart_item.original_cost.value, 'current_cost': cart_item.current_cost.value, 'dtd': { 'occasion': { 'name': dtd.occasion.name.value, 'description': dtd.occasion.description.value, } if dtd.occasion else None, 'date_from': dtd.date_from.strftime('%Y-%m-%d'), 'date_to': dtd.date_to.strftime('%Y-%m-%d'), 'working_days_from': dtd.working_days_from, 'working_days_to': dtd.working_days_to, }, 'fbucks': item_fbucks, }) return { 'items': items_data, 'original_subtotal': original_subtotal, 'current_subtotal': current_subtotal, 'current_subtotal_vat_amount': current_subtotal_vat_amount, 'available_fbucks_amount': available_fbucks_amount, }
class _CustomerTiersElasticStorage(CustomerTierStorageInterface): __ENTITY_PROPERTY_ID = '__id' __ENTITY_PROPERTY_NAME = '__name' __ENTITY_PROPERTY_CREDIT_BACK_PERCENT = '__credit_back_percent' __ENTITY_PROPERTY_SPENT_AMOUNT_MIN = 'spent_amount_min' __ENTITY_PROPERTY_SPENT_AMOUNT_MAX = 'spent_amount_max' __ENTITY_PROPERTY_IS_DELETED = '__is_deleted' def __init__(self): """ curl -X DELETE localhost:9200/customer_tiers_tiers curl -X PUT localhost:9200/customer_tiers_tiers -H "Content-Type: application/json" -d'{ "mappings": { "customer_tiers_tiers": { "properties": { "id": {"type": "integer"}, "name": {"type": "keyword"}, "credit_back_percent": {"type": "integer"}, "spent_amount_min": {"type": "integer"}, "spent_amount_max": {"type": "integer"}, "is_deleted": {"type": "boolean"} } } } }' """ self.__elastic = Elastic( settings.AWS_ELASTICSEARCH_CUSTOMER_TIERS_TIERS, settings.AWS_ELASTICSEARCH_CUSTOMER_TIERS_TIERS ) self.__reflector = Reflector() def save(self, entity: CustomerTier) -> None: entity_data = self.__reflector.extract(entity, [ self.__class__.__ENTITY_PROPERTY_ID, self.__class__.__ENTITY_PROPERTY_NAME, self.__class__.__ENTITY_PROPERTY_CREDIT_BACK_PERCENT, self.__class__.__ENTITY_PROPERTY_SPENT_AMOUNT_MIN, self.__class__.__ENTITY_PROPERTY_SPENT_AMOUNT_MAX, self.__class__.__ENTITY_PROPERTY_IS_DELETED ]) document_id = entity_data[self.__class__.__ENTITY_PROPERTY_ID].value document_data = { 'id': entity_data[self.__class__.__ENTITY_PROPERTY_ID].value, 'name': entity_data[self.__class__.__ENTITY_PROPERTY_NAME].value, 'credit_back_percent': entity_data[self.__class__.__ENTITY_PROPERTY_CREDIT_BACK_PERCENT].value, 'spent_amount_min': entity_data[self.__class__.__ENTITY_PROPERTY_SPENT_AMOUNT_MIN], 'spent_amount_max': entity_data[self.__class__.__ENTITY_PROPERTY_SPENT_AMOUNT_MAX], 'is_deleted': entity_data[self.__class__.__ENTITY_PROPERTY_IS_DELETED], } if self.__elastic.get_data(document_id): self.__elastic.update_data(document_id, {'doc': document_data}) else: self.__elastic.create(document_id, document_data) def __create_entity(self, row: dict) -> CustomerTier: entity = self.__reflector.construct(CustomerTier, { self.__class__.__ENTITY_PROPERTY_ID: Id(str(row['id'])), self.__class__.__ENTITY_PROPERTY_NAME: Name(row['name']), self.__class__.__ENTITY_PROPERTY_CREDIT_BACK_PERCENT: Percentage(int(row['credit_back_percent'])), self.__class__.__ENTITY_PROPERTY_SPENT_AMOUNT_MIN: int(row['spent_amount_min']), self.__class__.__ENTITY_PROPERTY_SPENT_AMOUNT_MAX: int(row['spent_amount_max']), self.__class__.__ENTITY_PROPERTY_IS_DELETED: row['is_deleted'], }) return entity def get_by_id(self, tier_id: Id) -> Optional[CustomerTier]: if not isinstance(tier_id, Id): raise ArgumentTypeException(self.get_by_id, 'tier_id', tier_id) row = self.__elastic.get_data(tier_id.value) return self.__create_entity(row) if row else None def get_all(self) -> Tuple[CustomerTier]: rows = self.__elastic.post_search({'query': {'match_all': {}}}).get('hits', {}).get('hits') result = [self.__create_entity(row['_source']) for row in rows] result = [entity for entity in result if not entity.is_deleted] result = tuple(result) return result def get_neutral(self) -> CustomerTier: for tier in self.get_all(): if tier.is_neutral: return tier else: raise ApplicationLogicException('Neutral Tier does not exist!')
class _ReturnRequestStorageElastic(ReturnRequestStorageInterface): """ curl -X DELETE localhost:9200/purchase_return_requests curl -X PUT localhost:9200/purchase_return_requests -H "Content-Type: application/json" -d'{ "mappings": { "purchase_return_requests": { "properties": { "request_number": {"type": "keyword"}, "customer_id": {"type": "keyword"}, "request_items": { "properties": { "order_number": {"type": "keyword"}, "simple_sku": {"type": "keyword"}, "qty": {"type": "integer"}, "cost": {"type": "float"}, "reason": {"type": "keyword"}, "additional_comment": {"type": "keyword"}, "attached_files_urls_json": {"type": "keyword"}, "status_history": { "properties": { "status": {"type": "keyword"}, "datetime": {"type": "date", "format": "date_hour_minute_second_millis"} } } } }, "delivery_method": {"type": "keyword"}, "refund_method": {"type": "keyword"}, "refund_method_extra_data_json": {"type": "keyword"} } } } }' curl -X DELETE localhost:9200/purchase_return_requests_customer_map curl -X PUT localhost:9200/purchase_return_requests_customer_map -H "Content-Type: application/json" -d'{ "mappings": { "purchase_return_requests_customer_map": { "properties": { "request_numbers_json": {"type": "keyword"} } } } }' """ __ENTITY_PROPERTY_REQUEST_NUMBER = '__number' __ENTITY_PROPERTY_CUSTOMER_ID = '__customer_id' __ENTITY_PROPERTY_REFUND_METHOD = '__refund_method' __ENTITY_PROPERTY_DELIVERY_METHOD = '__delivery_method' __ENTITY_PROPERTY_ITEMS = '__items' __ENTITY_PROPERTY_ITEMS_ORDER_NUMBER = '__order_number' __ENTITY_PROPERTY_ITEMS_SIMPLE_SKU = '__simple_sku' __ENTITY_PROPERTY_ITEMS_QTY = '__qty' __ENTITY_PROPERTY_ITEMS_COST = '__cost' __ENTITY_PROPERTY_ITEMS_REASON = '__reason' __ENTITY_PROPERTY_ITEMS_ATTACHED_FILES = '__attached_files' __ENTITY_PROPERTY_ITEMS_ADDITIONAL_COMMENT = '__additional_comment' __ENTITY_PROPERTY_ITEMS_STATUS_HISTORY = '__status_history' def __init__(self): self.__requests_elastic = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_RETURN_REQUESTS, settings.AWS_ELASTICSEARCH_PURCHASE_RETURN_REQUESTS) self.__customer_requests_map_elastic = Elastic( settings.AWS_ELASTICSEARCH_PURCHASE_RETURN_REQUESTS_CUSTOMER_MAP, settings.AWS_ELASTICSEARCH_PURCHASE_RETURN_REQUESTS_CUSTOMER_MAP) self.__reflector = Reflector() def save(self, return_request: ReturnRequest) -> None: if not isinstance(return_request, ReturnRequest): raise ArgumentTypeException(self.save, 'return_request', return_request) items_data = [] for item in return_request.items: status_history: ReturnRequest.Item.StatusChangesHistory = self.__reflector.extract( item, (self.__class__.__ENTITY_PROPERTY_ITEMS_STATUS_HISTORY, ))[self.__class__.__ENTITY_PROPERTY_ITEMS_STATUS_HISTORY] items_data.append({ "order_number": item.order_number.value, "simple_sku": item.simple_sku.value, "qty": item.qty.value, "cost": item.cost.value, "reason": item.reason.descriptor, "additional_comment": item.additional_comment.value if item.additional_comment else None, "attached_files_urls_json": json.dumps([file.url for file in item.attached_files]), "status_history": [ { 'status': status_change.status.value, # elastic supports only 3 digits for milliseconds 'datetime': status_change.datetime.strftime('%Y-%m-%dT%H:%M:%S.%f') [:-3], } for status_change in status_history.get_all() ] }) document_id = return_request.number.value document_data = { "request_number": return_request.number.value, "customer_id": return_request.customer_id.value, "request_items": items_data, "delivery_method": return_request.delivery_method.descriptor, "refund_method": return_request.refund_method.descriptor, "refund_method_extra_data_json": json.dumps(return_request.refund_method.extra_data), } existed_request = self.load(return_request.number) if existed_request: # just a double check of number uniqueness if existed_request.customer_id != return_request.customer_id: raise RuntimeError( 'Return Request "{}" already exists and belongs to another Customer!' .format(return_request.number)) self.__requests_elastic.update_data(document_id, {'doc': document_data}) else: self.__requests_elastic.create(document_id, document_data) # Elastic can search by attributes only after 1 second from last update. # We need all data, when we are searching by customer_id, # so in this case we will lost fresh data, if search directly after creation of a new return request. # In this case we need to use another index and get data by elastic doc_id. customer_requests_map = self.__customer_requests_map_elastic.get_data( return_request.customer_id.value) if customer_requests_map: request_numbers = list( json.loads( customer_requests_map.get('request_numbers_json', '[]')) or []) request_numbers.append(return_request.number.value) request_numbers = list(set(request_numbers)) self.__customer_requests_map_elastic.update_data( return_request.customer_id.value, { 'doc': { 'request_numbers_json': json.dumps(request_numbers) } }) else: self.__customer_requests_map_elastic.create( return_request.customer_id.value, { 'request_numbers_json': json.dumps([return_request.number.value]) }) def load(self, request_number: ReturnRequest.Number) -> Optional[ReturnRequest]: if not isinstance(request_number, ReturnRequest.Number): raise ArgumentTypeException(self.load, 'request_number', request_number) data = self.__requests_elastic.get_data(request_number.value) result = self.__restore(data) if data else None return result def __restore(self, data: dict) -> ReturnRequest: request_items = [] for item_data in data['request_items']: attached_files = json.loads(item_data['attached_files_urls_json']) attached_files = tuple([ ReturnRequest.Item.AttachedFile(url) for url in attached_files ]) additional_comment = ReturnRequest.Item.AdditionalComment( item_data['additional_comment']) status_history = ReturnRequest.Item.StatusChangesHistory( tuple([ self.__reflector.construct( ReturnRequest.Item.StatusChangesHistory.Change, { '__status': ReturnRequest.Item.Status(change['status']), # elastic supports only 3 digits for milliseconds '__datetime': datetime.datetime.strptime( change['datetime'] + '000', '%Y-%m-%dT%H:%M:%S.%f'), }) for change in item_data['status_history'] ])) request_items.append( self.__reflector.construct( ReturnRequest.Item, { self.__class__.__ENTITY_PROPERTY_ITEMS_ORDER_NUMBER: OrderNumber(item_data['order_number']), self.__class__.__ENTITY_PROPERTY_ITEMS_SIMPLE_SKU: SimpleSku(item_data['simple_sku']), self.__class__.__ENTITY_PROPERTY_ITEMS_QTY: Qty(item_data['qty']), self.__class__.__ENTITY_PROPERTY_ITEMS_COST: Cost(item_data['cost']), self.__class__.__ENTITY_PROPERTY_ITEMS_REASON: ReturnRequest.Item.Reason(item_data['reason']), self.__class__.__ENTITY_PROPERTY_ITEMS_ATTACHED_FILES: attached_files, self.__class__.__ENTITY_PROPERTY_ITEMS_ADDITIONAL_COMMENT: additional_comment, self.__class__.__ENTITY_PROPERTY_ITEMS_STATUS_HISTORY: status_history, })) return_request = self.__reflector.construct( ReturnRequest, { self.__class__.__ENTITY_PROPERTY_REQUEST_NUMBER: ReturnRequest.Number(data['request_number']), self.__class__.__ENTITY_PROPERTY_CUSTOMER_ID: Id(data['customer_id']), self.__class__.__ENTITY_PROPERTY_ITEMS: tuple(request_items), self.__class__.__ENTITY_PROPERTY_DELIVERY_METHOD: _restore_delivery_method(data['delivery_method']), self.__class__.__ENTITY_PROPERTY_REFUND_METHOD: _restore_refund_method( data['refund_method'], json.loads(data['refund_method_extra_data_json'])), }) return return_request def get_all_for_customer(self, customer_id: Id) -> Tuple[ReturnRequest]: if not isinstance(customer_id, Id): raise ArgumentTypeException(self.get_all_for_customer, 'customer_id', customer_id) data = self.__customer_requests_map_elastic.get_data(customer_id.value) request_numbers = json.loads(( data.get('request_numbers_json') or '[]') if data else '[]') or [] if not request_numbers: return tuple() rows = self.__requests_elastic.post_search({ "query": { "ids": { "values": request_numbers } }, "size": 10000 }).get('hits', {}).get('hits', []) or [] result = [self.__restore(row['_source']) for row in rows] if len(result) != len(request_numbers): message = '{} can\'t find all Return-Requests for Customer #{}! Not existed Return-Requests in map: {}' raise ValueError( message.format(self.get_all_for_customer, customer_id.value, [ request_number for request_number in request_numbers if request_number not in [request.number.value for request in result] ])) return tuple(result)
class CustomerTiersCustomersSqsHandler(SqsHandlerInterface): def __init__(self): self.__tiers_storage = CustomerTierStorageImplementation() self.__messages = MessageStorageImplementation() self.__logger = Logger() """""" # @todo : refactoring from chalicelib.libs.purchase.core import CustomerInterface from chalicelib.libs.purchase.customer.storage import CustomerStorageImplementation see = CustomerInterface.tier see = CustomerStorageImplementation.save """""" self.__elastic = Elastic( settings.AWS_ELASTICSEARCH_CUSTOMER_TIERS_CUSTOMER_TIERS, settings.AWS_ELASTICSEARCH_CUSTOMER_TIERS_CUSTOMER_TIERS) def handle(self, sqs_message: SqsMessage) -> None: # 'tiers' here are the same tiers set as in 'customer_tiers_set' sqs-message. # Theoretically this message can be handled earlier than 'customer_tiers_set' message, # so we need to be sure, that all new tiers exist. incoming_tiers_ids = [ row['id'] for row in sqs_message.message_data['tiers'] ] stored_tiers_ids = [ tier.id.value for tier in self.__tiers_storage.get_all() ] if sum([ tier_id for tier_id in incoming_tiers_ids if tier_id not in stored_tiers_ids ]) > 0: # @todo : this is a crutch CustomerTiersTiersSqsHandler().handle( SqsMessage(sqs_message.id, 'customer_tiers_set', {'tiers': sqs_message.message_data['tiers']})) # assign customers to tiers tiers = self.__tiers_storage.get_all() tiers_map = {} for tier in tiers: tiers_map[tier.id.value] = tier for customer_tier_data in sqs_message.message_data.get('customers'): customer_email = str(customer_tier_data['email']) tier_id = int(customer_tier_data['tier_id']) if self.__elastic.get_data(customer_email): self.__elastic.update_data(customer_email, {'doc': { 'tier_id': tier_id }}) else: self.__elastic.create(customer_email, {'tier_id': tier_id}) # notify user (silently) try: tier = tiers_map[str(tier_id)] self.__messages.save( Message( str(uuid.uuid4()), customer_email, 'Your Customer Tier has been changed!', 'Now you are in the "{}" Customer Tier!'.format( tier.name.value))) except BaseException as e: self.__logger.log_exception(e)
def handle(self, sqs_message: SqsMessage) -> None: """ crutch customer_info_request_answer [ 'customer_email' => $customer->getEmail(), 'name' => [ 'first' => $customer->getFirstName() ?: null, 'last' => $customer->getLastName() ?: null, ], 'gender' => 'F' / 'M', 'addresses' => [ [ 'nickname' => $address->getAddressNickname() ?: null, 'phone' => $address->getTelephone() ?: $address->getContactNumber() ?: null, 'street' => $address->getStreet() ?: null, 'suburb' => $address->getSuburb() ?: null, 'post_code' => $address->getPostCode() ?: null, 'city' => $address->getCity() ?: null, 'province' => $address->getProvince() ?: null, 'country_code' => $address->getCountryCode() ?: null, 'is_default_billing' => $address->getId() == $customer->getDefaultBillingAddressId(), 'is_default_shipping' => $address->getId() == $customer->getDefaultShippingAddressId(), ], ... ], 'tier' => [ 'id' => $customerTier->getId(), 'name' => $customerTier->getName(), 'credit_back_percent' => $customerTier->getCreditBackPercent(), 'spent_amount_min' => $customerTier->getSpendAmountMin(), 'spent_amount_max' => $customerTier->getSpendAmountMax(), ] ] """ # @todo : perhaps, update other data - needs to be able to get customer by email # 'tier' here is the same tier as in 'customer_tiers_set' sqs-message. # Theoretically this message can be handled earlier than 'customer_tiers_set' message, # so we need to be sure, that all new tiers exist. tier_data = sqs_message.message_data['tier'] tier = self.__tiers_storage.get_by_id(Id(str(tier_data['id']))) if not tier: tier = CustomerTier( Id(str(tier_data['id'])), Name(tier_data['name']), Percentage(int(tier_data['credit_back_percent'])), int(tier_data['spent_amount_min']), int(tier_data['spent_amount_max'])) self.__tiers_storage.save(tier) # assign user to tier """""" # @todo : refactoring from chalicelib.libs.purchase.core import CustomerInterface from chalicelib.libs.purchase.customer.storage import CustomerStorageImplementation see = CustomerInterface.tier see = CustomerStorageImplementation.save """""" customer_email = sqs_message.message_data['customer_email'] elastic = Elastic( settings.AWS_ELASTICSEARCH_CUSTOMER_TIERS_CUSTOMER_TIERS, settings.AWS_ELASTICSEARCH_CUSTOMER_TIERS_CUSTOMER_TIERS) if elastic.get_data(customer_email): elastic.update_data(customer_email, {'doc': { 'tier_id': tier.id.value }}) else: elastic.create(customer_email, {'tier_id': tier.id.value})