class StaticPageStorage(object): __ENTITY_PROPERTY_DESCRIPTOR = '__descriptor' __ENTITY_PROPERTY_NAME = '__name' __ENTITY_PROPERTY_CONTENT = '__content' def __init__(self): # is better to use composition instead of inheritance self.__storage = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__storage.PARTITION_KEY = 'STATIC_PAGE' self.__reflector = Reflector() def save(self, entity: StaticPage) -> None: if not isinstance(entity, StaticPage): raise ArgumentTypeException(self.save, 'entity', entity) data = self.__reflector.extract(entity, ( self.__class__.__ENTITY_PROPERTY_DESCRIPTOR, self.__class__.__ENTITY_PROPERTY_NAME, self.__class__.__ENTITY_PROPERTY_CONTENT, )) self.__storage.put_item( data[self.__class__.__ENTITY_PROPERTY_DESCRIPTOR], { 'name': data[self.__class__.__ENTITY_PROPERTY_NAME], 'content': data[self.__class__.__ENTITY_PROPERTY_CONTENT] }) def __get_instance(self, data: dict) -> StaticPage: entity: StaticPage = self.__reflector.construct( StaticPage, { self.__class__.__ENTITY_PROPERTY_DESCRIPTOR: data['sk'], self.__class__.__ENTITY_PROPERTY_NAME: data['name'], self.__class__.__ENTITY_PROPERTY_CONTENT: data['content'], }) return entity def get_by_descriptor(self, descriptor: str) -> Optional[StaticPage]: if not isinstance(descriptor, str): raise ArgumentTypeException(self.get_by_descriptor, 'descriptor', descriptor) elif not str(descriptor).strip(): raise ArgumentCannotBeEmptyException(self.get_by_descriptor, 'descriptor') data = self.__storage.find_item(descriptor) return self.__get_instance(data) if data else None def remove(self, descriptor: str) -> None: if not isinstance(descriptor, str): raise ArgumentTypeException(self.get_by_descriptor, 'descriptor', descriptor) elif not str(descriptor).strip(): raise ArgumentCannotBeEmptyException(self.get_by_descriptor, 'descriptor') self.__storage.delete_item(descriptor)
class _CreditCardsStorageDynamoDb(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): self.__dynamo_db = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__dynamo_db.PARTITION_KEY = 'PURCHASE_CUSTOMER_CREDIT_CARDS' self.__reflector = Reflector() def __restore(self, row: dict) -> CreditCard: card = self.__reflector.construct( CreditCard, { self.__class__.__ENTITY_PROPERTY_TOKEN: row['sk'], 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.__dynamo_db.find_item(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') items = self.__dynamo_db.find_by_attribute('customer_id', customer_id) result = [self.__restore(item) for item in items] 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, ]) self.__dynamo_db.put_item( 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') }) def remove(self, card: CreditCard) -> None: if not isinstance(card, CreditCard): raise ArgumentTypeException(self.remove, 'card', card) if not self.__dynamo_db.find_item(card.token): raise ArgumentValueException('Card #{} is already Removed!'.format( card.token)) self.__dynamo_db.delete_item(card.token)
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 SubscriptionStorage: __ENTITY_PROPERTY_SUBSCRIPTION_ID = '__subscription_id' __ENTITY_PROPERTY_EMAIL = '__email' __ENTITY_PROPERTY_USER_ID = '__user_id' __ENTITY_PROPERTY_SUBSCRIBED_AT = '__subscribed_at' __ENTITY_PROPERTY_UNSUBSCRIBED_AT = '__unsubscribed_at' def __init__(self): # is better to use composition instead of inheritance self.__storage = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__storage.PARTITION_KEY = 'EMAIL_SUBSCRIPTION' self.__reflector = Reflector() def __get_instance(self, data: dict) -> Subscription: subscription_id = Id(data.get('sk')) email = Email(data.get('email')) user_id = Id(data.get('user_id')) if data.get('user_id') else None subscribed_at = datetime.strptime(data.get('subscribed_at'), '%Y-%m-%d %H:%M:%S') unsubscribed_at = data.get('unsubscribed_at') or None unsubscribed_at = datetime.strptime(unsubscribed_at, '%Y-%m-%d %H:%M:%S') if unsubscribed_at else None entity: Subscription = self.__reflector.construct(Subscription, { self.__class__.__ENTITY_PROPERTY_SUBSCRIPTION_ID: subscription_id, self.__class__.__ENTITY_PROPERTY_EMAIL: email, self.__class__.__ENTITY_PROPERTY_USER_ID: user_id, self.__class__.__ENTITY_PROPERTY_SUBSCRIBED_AT: subscribed_at, self.__class__.__ENTITY_PROPERTY_UNSUBSCRIBED_AT: unsubscribed_at, }) return entity def get_by_id(self, subscription_id: Id) -> Optional[Subscription]: if not isinstance(subscription_id, Id): raise ArgumentTypeException(self.get_by_id, 'subscription_id', subscription_id) data = self.__storage.find_item(subscription_id.value) return self.__get_instance(data) if data else None def get_by_email(self, email: Email) -> Optional[Subscription]: if not isinstance(email, Email): raise ArgumentTypeException(self.get_by_email, 'email', email) data = (self.__storage.find_by_attribute('email', email.value) or [None])[0] or None return self.__get_instance(data) if data else None def save(self, entity: Subscription) -> None: if not isinstance(entity, Subscription): raise ArgumentTypeException(self.save, 'entity', entity) data = self.__reflector.extract(entity, ( self.__class__.__ENTITY_PROPERTY_SUBSCRIPTION_ID, self.__class__.__ENTITY_PROPERTY_EMAIL, self.__class__.__ENTITY_PROPERTY_USER_ID, self.__class__.__ENTITY_PROPERTY_SUBSCRIBED_AT, self.__class__.__ENTITY_PROPERTY_UNSUBSCRIBED_AT, )) subscription_id: Id = data[self.__class__.__ENTITY_PROPERTY_SUBSCRIPTION_ID] email: Email = data[self.__class__.__ENTITY_PROPERTY_EMAIL] user_id: Optional[Id] = data[self.__class__.__ENTITY_PROPERTY_USER_ID] subscribed_at: datetime = data[self.__class__.__ENTITY_PROPERTY_SUBSCRIBED_AT] unsubscribed_at: Optional[datetime] = data[self.__class__.__ENTITY_PROPERTY_UNSUBSCRIBED_AT] self.__storage.put_item(subscription_id.value, { 'email': email.value, 'user_id': user_id.value if user_id else None, 'subscribed_at': subscribed_at.strftime('%Y-%m-%d %H:%M:%S'), 'unsubscribed_at': unsubscribed_at.strftime('%Y-%m-%d %H:%M:%S') if unsubscribed_at else None })
class _MessageStorageDynamoDb(MessageStorageInterface): __ENTITY_PROPERTY_MESSAGE_ID = '__id' __ENTITY_PROPERTY_CUSTOMER_EMAIL = '__customer_email' __ENTITY_PROPERTY_TITLE = '__title' __ENTITY_PROPERTY_TEXT = '__text' __ENTITY_PROPERTY_CREATED_AT = '__created_at' def __init__(self): # is better to use composition instead of inheritance self.__storage = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__storage.PARTITION_KEY = 'NOTIFICATION_SIMPLE' self.__reflector = Reflector() def save(self, entity: Message) -> None: if not isinstance(entity, Message): raise ArgumentTypeException(self.save, 'entity', entity) data = self.__reflector.extract(entity, ( self.__class__.__ENTITY_PROPERTY_MESSAGE_ID, self.__class__.__ENTITY_PROPERTY_CUSTOMER_EMAIL, self.__class__.__ENTITY_PROPERTY_TITLE, self.__class__.__ENTITY_PROPERTY_TEXT, self.__class__.__ENTITY_PROPERTY_CREATED_AT, )) self.__storage.put_item( data[self.__class__.__ENTITY_PROPERTY_MESSAGE_ID], { 'customer_email': data[self.__class__.__ENTITY_PROPERTY_CUSTOMER_EMAIL], 'title': data[self.__class__.__ENTITY_PROPERTY_TITLE], 'text': data[self.__class__.__ENTITY_PROPERTY_TEXT], 'created_at': data[self.__class__.__ENTITY_PROPERTY_CREATED_AT].strftime( '%Y-%m-%d %H:%M:%S') }) def remove(self, message_id: str) -> None: if not isinstance(message_id, str): raise ArgumentTypeException(self.remove, 'message_id', message_id) elif not str(message_id).strip(): raise ArgumentCannotBeEmptyException(self.remove, 'message_id') self.__storage.delete_item(message_id) def get_all_for_customer(self, customer_email: str) -> Tuple[Message]: if not isinstance(customer_email, str): raise ArgumentTypeException(self.get_all_for_customer, 'customer_email', customer_email) elif not str(customer_email).strip(): raise ArgumentCannotBeEmptyException(self.get_all_for_customer, 'customer_email') rows = self.__storage.filter_by_field_value('customer_email', customer_email) return tuple([self.__get_instance(row) for row in rows]) def __get_instance(self, data: dict) -> Message: entity: Message = self.__reflector.construct( Message, { self.__class__.__ENTITY_PROPERTY_MESSAGE_ID: data.get('sk'), self.__class__.__ENTITY_PROPERTY_CUSTOMER_EMAIL: data.get('customer_email'), self.__class__.__ENTITY_PROPERTY_TITLE: data.get('title'), self.__class__.__ENTITY_PROPERTY_TEXT: data.get('text'), self.__class__.__ENTITY_PROPERTY_CREATED_AT: datetime.strptime(data.get('created_at'), '%Y-%m-%d %H:%M:%S') }) return entity
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 _CancelRequestStorageDynamoDb(CancelRequestStorageInterface): __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.__dynamo_db = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__dynamo_db.PARTITION_KEY = 'PURCHASE_CANCELLATION_REQUEST' self.__reflector = Reflector() def save(self, cancel_request: CancelRequest) -> None: if not isinstance(cancel_request, CancelRequest): raise ArgumentTypeException(self.save, 'cancel_request', cancel_request) self.__dynamo_db.put_item( cancel_request.number.value, { "order_number": cancel_request.order_number.value, "requested_at": cancel_request.requested_at.strftime('%Y-%m-%dT%H:%M:%S.%f'), "request_items": [{ "simple_sku": item.simple_sku.value, "qty": item.qty.value, "status": item.status.value, "processed_at": item.processed_at.strftime('%Y-%m-%dT%H:%M:%S.%f') if item.processed_at else None } for item in cancel_request.items], 'refund_method': cancel_request.refund_method.descriptor, 'refund_method_extra_data_json': json.dumps(cancel_request.refund_method.extra_data), "additional_comment": cancel_request.additional_comment.value if cancel_request.additional_comment else None }) def __restore(self, data: dict) -> CancelRequest: cancel_request = self.__reflector.construct( CancelRequest, { self.__class__.__ENTITY_PROPERTY_REQUEST_NUMBER: CancelRequest.Number(data['sk']), 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'], '%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'], '%Y-%m-%dT%H:%M:%S.%f'), }) return cancel_request 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.__dynamo_db.find_item(request_number.value) return self.__restore(data) if data else None 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) items = self.__dynamo_db.find_by_attribute('order_number', order_number.value) result = [self.__restore(item) for item in items] return tuple(result)
class _OrderStorageDynamoDb(OrderStorageInterface): def __init__(self): self.__dynamo_db = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__dynamo_db.PARTITION_KEY = 'PURCHASE_ORDERS' self.__reflector = Reflector() 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 = { '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_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_return_requested': item.qty_return_requested.value, 'qty_return_returned': item.qty_return_returned.value, 'qty_refunded': item.qty_refunded.value, 'qty_modified_at': item.qty_modified_at.strftime('%Y-%m-%dT%H:%M:%S.%f'), 'fbucks_earnings': 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, 'datetime': status_change.datetime.strftime('%Y-%m-%dT%H:%M:%S.%f'), } for status_change in status_changes], } # fix of "TypeError: Float types are not supported. Use Decimal types instead." error document_data = json.loads(json.dumps(document_data), parse_float=Decimal) self.__dynamo_db.put_item(document_id, document_data) def __restore(self, data: dict) -> Order: order_number = Order.Number(data.get('sk')) customer_id = Id(data.get('customer_id')) delivery_cost = Cost(float(data.get('delivery_cost'))) vat_percent = Percentage(float(data.get('vat_percent'))) credits_spent = Cost(float(data.get('credits_spent') or '0')) 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')) changed_at = datetime.datetime.strptime( status_change_data.get('datetime'), '%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_earnings')) 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)) qty_modified_at = datetime.datetime.strptime( item_data.get('qty_modified_at'), '%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 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.__dynamo_db.find_item(order_number.value) return self.__restore(data) if data else None def get_all_by_numbers(self, order_numbers: Tuple[Order.Number]) -> Tuple[Order]: if any([ not isinstance(order_number, Order.Number) for order_number in order_numbers ]): raise ArgumentTypeException(self.get_all_by_numbers, 'order_numbers', order_numbers) result = [self.load(order_number) for order_number in order_numbers] result = [order for order in result if order is not None] 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) items = self.__dynamo_db.find_by_attribute('customer_id', customer_id.value) result = [self.__restore(item) for item in items] 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 _CartDynamoDbStorage(DynamoModel, CartStorageInterface): TABLE_NAME = settings.AWS_DYNAMODB_CMS_TABLE_NAME PARTITION_KEY = 'PURCHASE_CART' __ENTITY_PROPERTY_ID = '__id' __ENTITY_PROPERTY_ITEMS = '__items' __ENTITY_PROPERTY_ITEMS_PRODUCT = '__product' __ENTITY_PROPERTY_ITEMS_QTY = '__qty' __ENTITY_PROPERTY_VAT_PERCENT = '__vat_percent' def __init__(self, product_storage: ProductStorageInterface): if not isinstance(product_storage, ProductStorageInterface): raise ArgumentTypeException(self.__init__, 'product_storage', product_storage) super(self.__class__, self).__init__(self.TABLE_NAME) self.__product_storage = product_storage self.__vat_percent = Percentage(PurchaseSettings().vat) self.__reflector = Reflector() # ------------------------------------------------------------------------------------------------------------------ def save(self, cart: Cart) -> None: if not isinstance(cart, Cart): raise ArgumentTypeException(self.save, 'cart', cart) data = { 'pk': self.PARTITION_KEY, 'sk': cart.cart_id.value, 'cart_items': [{ 'simple_sku': item.simple_sku.value, 'qty': item.qty.value, } for item in cart.items] } # insert or update self.table.put_item(Item=data) # ------------------------------------------------------------------------------------------------------------------ def get_by_id(self, cart_id: Id) -> Optional[Cart]: if not isinstance(cart_id, Id): raise ArgumentTypeException(self.get_by_id, 'cart_id', cart_id) data = self.get_item(cart_id.value).get('Item', None) result = self.__restore(data) if data else None return result def __restore(self, data: dict) -> Cart: cart_items = [] for item_data in data.get('cart_items', tuple()): simple_sku = SimpleSku(str(item_data.get('simple_sku'))) qty = Qty(int(item_data.get('qty'))) product = self.__product_storage.load(simple_sku) cart_items.append( self.__reflector.construct( Cart.Item, { self.__class__.__ENTITY_PROPERTY_ITEMS_PRODUCT: product, self.__class__.__ENTITY_PROPERTY_ITEMS_QTY: qty, })) cart: Cart = self.__reflector.construct( Cart, { self.__class__.__ENTITY_PROPERTY_ID: Id(data.get('sk')), self.__class__.__ENTITY_PROPERTY_ITEMS: cart_items, self.__class__.__ENTITY_PROPERTY_VAT_PERCENT: self.__vat_percent }) return cart
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 _CustomerTiersStorageDynamoDb(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): self.__dynamo_db = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__dynamo_db.PARTITION_KEY = 'PURCHASE_CUSTOMER_TIERS_TIER' 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 = { '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], } # fix of "TypeError: Float types are not supported. Use Decimal types instead." error document_data = json.loads(json.dumps(document_data), parse_float=Decimal) self.__dynamo_db.put_item(document_id, document_data) def __restore(self, row: dict) -> CustomerTier: entity = self.__reflector.construct(CustomerTier, { self.__class__.__ENTITY_PROPERTY_ID: Id(str(row['sk'])), 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.__dynamo_db.find_item(tier_id.value) return self.__restore(row) if row else None def get_all(self) -> Tuple[CustomerTier]: rows = self.__dynamo_db.find_all() result = [self.__restore(row) 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 _ReturnRequestStorageDynamoDb(ReturnRequestStorageInterface): __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.__dynamo_db = DynamoModel(settings.AWS_DYNAMODB_CMS_TABLE_NAME) self.__dynamo_db.PARTITION_KEY = 'PURCHASE_RETURN_REQUESTS' 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, 'datetime': status_change.datetime.strftime('%Y-%m-%dT%H:%M:%S.%f'), } for status_change in status_history.get_all()] }) document_id = return_request.number.value document_data = { '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), } # fix of "TypeError: Float types are not supported. Use Decimal types instead." error document_data = json.loads(json.dumps(document_data), parse_float=Decimal) self.__dynamo_db.put_item(document_id, document_data) 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.__dynamo_db.find_item(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']), '__datetime': datetime.datetime.strptime(change['datetime'], '%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) items = self.__dynamo_db.find_by_attribute('customer_id', customer_id.value) result = [self.__restore(item) for item in items] return tuple(result)