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 _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) }})
def customer_credits_checkout(): checkout_storage = CheckoutStorageImplementation() order_storage = OrderStorageImplementation() order_app_service = OrderAppService() cart_service = CartAppService() checkout_service = CheckoutAppService() sqs_sender = SqsSenderImplementation() logger = Logger() try: user = __get_user() # @todo : refactoring checkout = checkout_storage.load(Id(user.id)) if not checkout: raise ApplicationLogicException('Checkout does not exist!') elif checkout.total_due.value != 0: raise ApplicationLogicException('Unable to checkout not 0 amount with Customer Credits!') order = order_app_service.get_waiting_for_payment_by_checkout_or_checkout_new(user.id) def __log_flow(text: str) -> None: logger.log_simple('Customer Credits Payment Log for Order #{} : {}'.format( order.number.value, text )) __log_flow('Start') try: __log_flow('Credits Spending...') # Attention! # Currently we use f-bucks only! Other credits are not available for now! # @todo : other credit types # @todo : copy-paste code # @todo : when reservation of credits amount will be done, perhaps, use sqs to spend credits """""" from chalicelib.libs.purchase.core import Checkout see = Checkout.__init__ """""" # @TODO : refactoring : raw data usage import uuid import datetime 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_customer_amount_changes_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, ) fbucks_customer_amount_elastic.update_data(order.customer_id.value, { 'script': 'ctx._source.amount -= ' + str(order.credit_spent_amount.value) }) fbucks_customer_amount_changes_elastic.create(str(uuid.uuid4()) + str(order.customer_id.value), { "customer_id": order.customer_id.value, "amount": -order.credit_spent_amount.value, "changed_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "order_number": order.number.value, }) __log_flow('Credits Spent!') __log_flow('Order Updating...') order.payment_method = CustomerCreditsOrderPaymentMethod() order.status = Order.Status(Order.Status.PAYMENT_SENT) order.status = Order.Status(Order.Status.PAYMENT_RECEIVED) order_storage.save(order) __log_flow('Order Updated!') except BaseException as e: __log_flow('Not done because of Error : {}'.format(str(e))) raise e # send order update to sqs try: __log_flow('Order Change SQS Sending...') sqs_sender.send(OrderChangeSqsSenderEvent(order)) __log_flow('Order Change SQS Sent!') except BaseException as e: __log_flow('Order Change SQS NOT Sent because of Error: {}!'.format(str(e))) logger.log_exception(e) # flush cart try: __log_flow('Cart Flushing...') cart_service.clear_cart(user.session_id) __log_flow('Cart Flushed!') except BaseException as e: __log_flow('Cart NOT Flushed because of Error: {}'.format(str(e))) logger.log_exception(e) # flush checkout try: __log_flow('Checkout Flushing...') checkout_service.remove(user.id) __log_flow('Checkout Flushed!') except BaseException as e: __log_flow('Checkout NOT Flushed because of Error: {}'.format(str(e))) logger.log_exception(e) result = { 'order_number': order.number.value } __log_flow('End') return result except BaseException as e: logger.log_exception(e) return http_response_exception_or_throw(e)
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)
def regular_eft_checkout(): checkout_storage = CheckoutStorageImplementation() order_storage = OrderStorageImplementation() order_app_service = OrderAppService() logger = Logger() mailer = MailerImplementation() # 1. Get or create order. Critical! # ------------------------------------------------------ try: user = __get_user() # @todo : refactoring checkout = checkout_storage.load(Id(user.id)) if not checkout: raise ApplicationLogicException('Checkout does not exist!') elif checkout.total_due.value == 0: raise ApplicationLogicException('Unable to checkout 0 amount with Regular Eft!') order = order_app_service.get_waiting_for_payment_by_checkout_or_checkout_new(user.id) def __log_order_flow(text: str) -> None: logger.log_simple('Regular EFT : Checkout : {} : {}'.format(order.number.value, text)) __log_order_flow('Start') # Attention! # Currently we use f-bucks only! Other credits are not available for now! # @todo : other credit types # @todo : copy-paste code # @todo : when reservation of credits amount will be done, perhaps, use sqs to spend credits if order.credit_spent_amount.value > 0: __log_order_flow('Spending Credits...') """""" from chalicelib.libs.purchase.core import Checkout see = Checkout.__init__ """""" # @TODO : refactoring : raw data usage import uuid import datetime 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_customer_amount_changes_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, ) fbucks_customer_amount_elastic.update_data(order.customer_id.value, { 'script': 'ctx._source.amount -= ' + str(order.credit_spent_amount.value), }) fbucks_customer_amount_changes_elastic.create(str(uuid.uuid4()) + str(order.customer_id.value), { "customer_id": order.customer_id.value, "amount": -order.credit_spent_amount.value, "changed_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "order_number": order.number.value, }) __log_order_flow('Spending Credits: Done!') __log_order_flow('Order Updating...') order.payment_method = RegularEftOrderPaymentMethod() order_storage.save(order) __log_order_flow('Order Updated!') except BaseException as e: logger.log_exception(e) return http_response_exception_or_throw(e) # 2. Send eft email. Not critical. # Theoretically can be redone or downloaded manually. # ------------------------------------------------------ try: __log_order_flow('EFT Email Sending...') message = RegularEftBankDetailsMailMessage(order) mailer.send(message) __log_order_flow('EFT Email Sent!') except BaseException as e: logger.log_exception(e) __log_order_flow('EFT Email Not Sent because of Error: {}'.format(str(e))) # 3. Flush cart, checkout. Not critical. # ------------------------------------------------------ # flush cart try: __log_order_flow('Cart Flushing...') from chalicelib.libs.purchase.cart.service import CartAppService cart_service = CartAppService() cart_service.clear_cart(user.session_id) __log_order_flow('Cart Flushed!') except BaseException as e: logger.log_exception(e) __log_order_flow('Cart Not Flushed because of Error: {}'.format(str(e))) # flush checkout try: __log_order_flow('Checkout Flushing...') from chalicelib.libs.purchase.checkout.service import CheckoutAppService checkout_service = CheckoutAppService() checkout_service.remove(user.id) __log_order_flow('Checkout Flushed!') except BaseException as e: logger.log_exception(e) __log_order_flow('Checkout Not Flushed because of Error: {}'.format(str(e))) return { 'order_number': order.number.value }
class OrderHandler(SqsHandlerInterface): def __init__(self) -> None: """ curl -X DELETE http://localhost:9200/personalization_orders curl -X PUT http://localhost:9200/personalization_orders -H "Content-Type: application/json" -d'{ "mappings": { "personalization_orders": { "properties": { "order_number": {"type": "keyword"}, "email": {"type": "keyword"}, "ordered_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, "rs_sku": {"type": "keyword"}, "rs_simple_sku": {"type": "keyword"}, "product_name": {"type": "keyword"}, "manufacturer": {"type": "keyword"}, "gender": {"type": "keyword"}, "product_size_attribute": {"type": "keyword"}, "rs_colour": {"type": "keyword"}, "size": {"type": "keyword"} } } } }' """ self.__elastic = Elastic( settings.AWS_ELASTICSEARCH_PERSONALIZATION_ORDERS, settings.AWS_ELASTICSEARCH_PERSONALIZATION_ORDERS) def handle(self, sqs_message: SqsMessage) -> None: order_number = str(sqs_message.message_data.get('order_number') or '').strip() or None order_items = sqs_message.message_data.get('order_items', []) or [] if not order_number or not order_items: raise ValueError('{} message data is incorrect: {}'.format( self.handle.__qualname__, sqs_message.message_data)) try: self.__elastic.delete_by_query( {"query": { "term": { "order_number": order_number } }}) emails = list() for order_item_data in order_items: emails.append(order_item_data.get('customer_email')) order_item = OrderItem( order_number, order_item_data.get('customer_email'), datetime.strptime(order_item_data.get('order_created_at'), '%Y-%m-%d %H:%M:%S'), order_item_data.get('product_config_sku'), order_item_data.get('product_simple_sku'), order_item_data.get('product_name'), order_item_data.get('product_brand_name'), order_item_data.get('product_gender_name'), order_item_data.get('product_type_name'), order_item_data.get('product_color_name'), order_item_data.get('product_size_name'), int(order_item_data.get('qty_ordered')), ) document_id = str(uuid4()) self.__elastic.create( document_id, { 'order_number': order_number, 'email': order_item.customer_email, 'ordered_at': order_item.ordered_at.strftime('%Y-%m-%d %H:%M:%S'), 'rs_sku': order_item.product_sku, 'rs_simple_sku': order_item.product_size_sku, 'product_name': order_item.product_name, 'manufacturer': order_item.product_brand_name, 'gender': order_item.product_gender_name, 'product_size_attribute': order_item.product_type_name, 'rs_colour': order_item.product_color_name, 'size': order_item.product_size_name, }) emails = list(set(emails)) User.send_calculate_product_score_for_customers(emails=emails) except BaseException as e: raise RuntimeError( '{} got an error, when tried to handle order {}. Error: {}'. format(self.handle.__qualname__, sqs_message.message_data, e))
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})
class FbucksChargeSqsHandler(SqsHandlerInterface): # @TODO : REFACTORING !!! currently we are working with raw data def __init__(self): self.__orders_storage = OrderStorageImplementation() self.__logger = Logger() # """ # curl -X DELETE localhost:9200/fbucks_handled_orders # curl -X PUT localhost:9200/fbucks_handled_orders -H "Content-Type: application/json" -d'{ # "mappings": { # "fbucks_handled_orders": { # "properties": { # "handled_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"} # } # } # } # }' # """ # self.__fbucks_handled_orders_elastic = Elastic( # settings.AWS_ELASTICSEARCH_FBUCKS_HANDLED_ORDERS, # settings.AWS_ELASTICSEARCH_FBUCKS_HANDLED_ORDERS, # ) self.__fbucks_handled_orders_dynamo_db = DynamoModel( settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__fbucks_handled_orders_dynamo_db.PARTITION_KEY = 'PURCHASE_FBUCKS_REWARD_HANDLED_ORDERS' # Attention! # We can get current customer's amount as a sum of all changes by customer_id # But theoretically elastic can not be in time with index update (1 second) between requests. # So there is another index to store amount value. """ curl -X DELETE localhost:9200/fbucks_customer_amount curl -X PUT localhost:9200/fbucks_customer_amount -H "Content-Type: application/json" -d'{ "mappings": { "fbucks_customer_amount": { "properties": { "amount": {"type": "integer"} } } } }' curl -X DELETE localhost:9200/fbucks_customer_amount_changes curl -X PUT localhost:9200/fbucks_customer_amount_changes -H "Content-Type: application/json" -d'{ "mappings": { "fbucks_customer_amount_changes": { "properties": { "customer_id": {"type": "keyword"}, "amount": {"type": "integer"}, "changed_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, "order_number": {"type": "keyword"} } } } }' """ self.__fbucks_customer_amount_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT, ) self.__fbucks_customer_amount_changes_elastic = Elastic( settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, settings.AWS_ELASTICSEARCH_FBUCKS_CUSTOMER_AMOUNT_CHANGES, ) self.__customer_storage = CustomerStorageImplementation() self.__messages_storage = MessageStorageImplementation() def handle(self, sqs_message: SqsMessage) -> None: import uuid import datetime from chalicelib.libs.purchase.core import Order order_number_values = sqs_message.message_data['order_numbers'] for order_number_value in order_number_values: try: now_string = datetime.datetime.now().strftime( "%Y-%m-%d %H:%M:%S") # skip duplicates # if self.__fbucks_handled_orders_elastic.get_data(order_number_value): if self.__fbucks_handled_orders_dynamo_db.find_item( order_number_value): self.__logger.log_simple( '{}: Fbucks for order #{} already earned!'.format( self.handle.__qualname__, order_number_value)) continue # ignore orders without fbucks amounts order = self.__orders_storage.load( Order.Number(order_number_value)) fbucks_amount = order.total_fbucks_earnings.value if fbucks_amount == 0: # remember order as handled # self.__fbucks_handled_orders_elastic.create(order_number_value, {'handled_at': now_string}) self.__fbucks_handled_orders_dynamo_db.put_item( order_number_value, {'handled_at': now_string}) continue # earn fbucks self.__fbucks_customer_amount_elastic.update_data( order.customer_id.value, { 'script': 'ctx._source.amount += ' + str(fbucks_amount), 'upsert': { 'amount': fbucks_amount, } }) self.__fbucks_customer_amount_changes_elastic.create( str(uuid.uuid4()) + str(order.customer_id.value), { "customer_id": order.customer_id.value, "amount": +fbucks_amount, "changed_at": now_string, "order_number": order_number_value, }) # remember order as handled # self.__fbucks_handled_orders_elastic.create(order_number_value, {'handled_at': now_string}) self.__fbucks_handled_orders_dynamo_db.put_item( order_number_value, {'handled_at': now_string}) # notify (silently) try: customer = self.__customer_storage.get_by_id( order.customer_id) self.__messages_storage.save( Message( str(uuid.uuid4()), customer.email.value, 'F-Bucks has been Earned!', 'You have earned {} F-Bucks by your Order #{}'. format(fbucks_amount, order.number.value))) except BaseException as e: self.__logger.log_exception(e) except BaseException as e: self.__logger.log_exception(e)