예제 #1
0
class MailService(object):
    """
    Mail Service class.
    """
    def __init__(self):
        self.consumers = Consumers('mail-service', [self.send])
        self.event_store = EventStoreClient()

    def start(self):
        logging.info('starting ...')
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.consumers.stop()
        logging.info('stopped.')

    def send(self, _req):
        if not _req['to'] or not _req['msg']:
            return {"error": "missing mandatory parameter 'to' and/or 'msg'"}

        # trigger event
        self.event_store.publish(
            'mail',
            create_event('mail_sent', {
                "recipient": _req['to'],
                "message": _req['msg']
            }))
class CustomerService(object):
    """
    Customer Service class.
    """
    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers('customer-service', [
            self.create_customers, self.update_customer, self.delete_customer
        ])

    @staticmethod
    def _create_entity(_name, _email):
        """
        Create a customer entity.

        :param _name: The name of the customer.
        :param _email: The em2ail address of the customer.
        :return: A dict with the entity properties.
        """
        return {'entity_id': str(uuid.uuid4()), 'name': _name, 'email': _email}

    def start(self):
        logging.info('starting ...')
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.consumers.stop()
        logging.info('stopped.')

    def create_customers(self, _req):
        customers = _req if isinstance(_req, list) else [_req]
        customer_ids = []

        for customer in customers:
            try:
                new_customer = CustomerService._create_entity(
                    customer['name'], customer['email'])
            except KeyError:
                return {
                    "error":
                    "missing mandatory parameter 'name' and/or 'email'"
                }

            # trigger event
            self.event_store.publish(
                'customer', create_event('entity_created', new_customer))

            customer_ids.append(new_customer['entity_id'])

        return {"result": customer_ids}

    def update_customer(self, _req):
        try:
            customer = CustomerService._create_entity(_req['name'],
                                                      _req['email'])
        except KeyError:
            return {
                "error": "missing mandatory parameter 'name' and/or 'email'"
            }

        try:
            customer['entity_id'] = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        # trigger event
        self.event_store.publish('customer',
                                 create_event('entity_updated', customer))

        return {"result": True}

    def delete_customer(self, _req):
        try:
            customer_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'customer',
            'id': customer_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        customer = rsp['result']
        if not customer:
            return {"error": "could not find customer"}

        # trigger event
        self.event_store.publish('customer',
                                 create_event('entity_deleted', customer))

        return {"result": True}
예제 #3
0
class BillingService(object):
    """
    Billing Service class.
    """
    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers('billing-service', [self.create_billings,
                                                       self.update_billing,
                                                       self.delete_billing])

    @staticmethod
    def _create_entity(_order_id, _amount):
        """
        Create a billing entity.

        :param _order_id: The order ID the billing belongs to.
        :param _amount: Total amount to pay.
        :return: A dict with the entity properties.
        """
        return {
            'entity_id': str(uuid.uuid4()),
            'order_id': _order_id,
            'amount': _amount
        }

    @staticmethod
    def _check_amount(_billing):
        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': _billing['order_id']})
        order = rsp['result']

        rsp = send_message('read-model', 'get_entity', {'name': 'cart', 'id': order['cart_id']})
        cart = rsp['result']

        rsp = send_message('read-model', 'get_entities', {'name': 'product', 'ids': cart['product_ids']})
        products = rsp['result']

        amount = sum([int(product['price']) for product in products])

        return amount == int(_billing['amount'])

    def start(self):
        logging.info('starting ...')
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.consumers.stop()
        logging.info('stopped.')

    def create_billings(self, _req):
        billings = _req if isinstance(_req, list) else [_req]
        billing_ids = []

        for billing in billings:
            res = self._check_amount(billing)
            if not res:
                return {
                    'error': 'amount is not accurate'
                }

            try:
                new_billing = BillingService._create_entity(billing['order_id'], billing['amount'])
            except KeyError:
                return {
                    "error": "missing mandatory parameter 'order_id' and/or 'amount'"
                }

            # trigger event
            self.event_store.publish('billing', create_event('entity_created', new_billing))

            billing_ids.append(new_billing['entity_id'])

        return {
            "result": billing_ids
        }

    def update_billing(self, _req):
        try:
            billing_id = _req['entity_id']
        except KeyError:
            return {
                "error": "missing mandatory parameter 'entity_id'"
            }

        rsp = send_message('read-model', 'get_entitiy', {'name': 'billing', 'id': billing_id})
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        billing = rsp['result']
        if not billing:
            return {
                "error": "could not find billing"
            }

        # set new props
        billing['entity_id'] = billing_id
        try:
            billing['order_id'] = _req['order_id']
            billing['amount'] = _req['amount']
        except KeyError:
            return {
                "result": "missing mandatory parameter 'order_id' and/or 'amount"
            }

        res = self._check_amount(billing)
        if not res:
            return {
                'error': 'amount is not accurate'
            }

        # trigger event
        self.event_store.publish('billing', create_event('entity_updated', billing))

        return {
            "result": True
        }

    def delete_billing(self, _req):
        try:
            billing_id = _req['entity_id']
        except KeyError:
            return {
                "error": "missing mandatory parameter 'entity_id'"
            }

        rsp = send_message('read-model', 'get_entitiy', {'name': 'billing', 'id': billing_id})
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        billing = rsp['result']
        if not billing:
            return {
                "error": "could not find billing"
            }

        # trigger event
        self.event_store.publish('billing', create_event('entity_deleted', billing))

        return {
            "result": True
        }
예제 #4
0
class ReadModel(object):
    """
    Read Model class.
    """

    def __init__(self, _redis_host='localhost', _redis_port=6379):
        self.event_store = EventStoreClient()
        self.consumers = Consumers('read-model', [self.get_entity,
                                                  self.get_entities,
                                                  self.get_mails,
                                                  self.get_unbilled_orders,
                                                  self.get_unshipped_orders,
                                                  self.get_delivered_orders])
        self.domain_model = DomainModel(
            redis.StrictRedis(host=_redis_host, port=_redis_port, decode_responses=True)
        )
        self.subscriptions = {}
        self.locks = {}

    @staticmethod
    def _deduce_entities(_events):
        """
        Deduce entities from events.

        :param _events: A list with events.
        :return: A dict mapping entity ID -> entity data.
        """
        if not _events:
            return {}

        # find 'created' events
        result = {json.loads(e[1]['event_data'])['entity_id']: json.loads(e[1]['event_data'])
                  for e in filter(lambda x: x[1]['event_action'] == 'entity_created', _events)}

        # remove 'deleted' events
        deleted = {json.loads(e[1]['event_data'])['entity_id']: json.loads(e[1]['event_data'])
                   for e in filter(lambda x: x[1]['event_action'] == 'entity_deleted', _events)}

        for d_id, d_data in deleted.items():
            del result[d_id]

        # change 'updated' events
        updated = {json.loads(e[1]['event_data'])['entity_id']: json.loads(e[1]['event_data'])
                   for e in filter(lambda x: x[1]['event_action'] == 'entity_updated', _events)}

        for u_id, u_data in updated.items():
            result[u_id] = u_data

        return result

    def _track_entities(self, _name, _event):
        """
        Keep track of entity events.

        :param _name: The entity name.
        :param _event: The event data.
        """
        if not self.domain_model.exists(_name):
            return

        entity = json.loads(_event.event_data)

        if _event.event_action == 'entity_created':
            self.domain_model.create(_name, entity)

        if _event.event_action == 'entity_deleted':
            self.domain_model.delete(_name, entity)

        if _event.event_action == 'entity_updated':
            self.domain_model.update(_name, entity)

    def _query_entities(self, _name):
        """
        Query all entities of a given name.

        :param _name: The entity name.
        :return: A dict mapping entity ID -> entity.
        """
        entities = self.domain_model.retrieve(_name)
        if entities:
            return entities

        if _name not in self.locks:
            self.locks[_name] = threading.Lock()

        with self.locks[_name]:
            entities = self.domain_model.retrieve(_name)
            if entities:
                return entities

            # deduce entities
            events = self.event_store.get(_name)
            entities = self._deduce_entities(events)

            # cache entities
            for entity in entities.values():
                self.domain_model.create(_name, entity)

            # track entities
            tracking_handler = functools.partial(self._track_entities, _name)
            self.event_store.subscribe(_name, tracking_handler)
            self.subscriptions[_name] = tracking_handler

            return entities

    def _query_defined_entities(self, _name, _props):
        """
        Query entities with defined properities.

        :param _name: The entity name.
        :param _props: A dict mapping property name -> property value(s).
        :return: A dict mapping entity ID -> entity.
        """
        result = {}
        for entity_id, entity in self._query_entities(_name).items():
            for prop_name, prop_value in _props.items():
                if not isinstance(prop_value, list):
                    prop_value = [prop_value]
                if prop_name in entity and entity[prop_name] in prop_value:
                    result[entity_id] = entity

        return result

    def _unbilled_orders(self):
        """
        Query all unbilled orders, i.e. orders w/o corresponding billing.

        :return: a dict mapping entity ID -> entity.
        """
        orders = self._query_entities('order')
        billings = self._query_entities('billing')

        unbilled = orders.copy()
        for billing_id, billing in billings.items():
            order_ids_to_remove = list(filter(lambda x: x == billing['order_id'], orders))
            if not order_ids_to_remove:
                raise Exception(f'could not find order {billing["order_id"]} for billing {billing_id}')

            if order_ids_to_remove[0] not in unbilled:
                raise Exception(f'could not find order {order_ids_to_remove[0]}')

            del unbilled[order_ids_to_remove[0]]

        return unbilled

    def _unshipped_orders(self):
        """
        Query all unshipped orders, i.e. orders w/o corresponding shipping done.

        :return: a dict mapping entity ID -> entity.
        """
        orders = self._query_entities('order')
        shippings = self._query_entities('shipping')

        unshipped = orders.copy()
        for shipping_id, shippings in shippings.items():
            order_ids_to_remove = list(filter(lambda x: x == shippings['order_id'], orders))
            if not order_ids_to_remove:
                raise Exception(f'could not find order {shippings["order_id"]} for shipping {shipping_id}')

            if order_ids_to_remove[0] not in unshipped:
                raise Exception(f'could not find order {order_ids_to_remove[0]}')

            del unshipped[order_ids_to_remove[0]]

        return unshipped

    def _delivered_orders(self):
        """
        Query all delivered orders.

        :return: a dict mapping entity ID -> entity.
        """
        shippings = self._query_entities('shipping')

        return list(filter(lambda x: x['delivered'], shippings.values()))

    def start(self):
        logging.info('starting ...')
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        for name, handler in self.subscriptions.items():
            self.event_store.unsubscribe(name, handler)
        self.consumers.stop()
        logging.info('stopped.')

    def get_entity(self, _req):
        if 'name' not in _req:
            return {
                "error": "missing mandatory parameter 'name'"
            }

        if 'id' in _req:
            return {
                'result': self._query_entities(_req['name']).get(_req['id'])
            }

        elif 'props' in _req and isinstance(_req['props'], dict):
            result = list(self._query_defined_entities(_req['name'], _req['props']).values())
            if len(result) <= 1:
                return {
                    'result': result[0] if result else None
                }
            else:
                return {
                    'error': 'more than 1 result found'
                }
        else:
            return {
                'result': 'invalid parameters'
            }

    def get_entities(self, _req):
        if 'name' not in _req:
            return {
                "error": "missing mandatory parameter 'name'"
            }

        elif 'ids' in _req and isinstance(_req['ids'], list):
            return {
                'result': [self._query_entities(_req['name']).get(_id) for _id in _req['ids']]
            }

        elif 'props' in _req and isinstance(_req['props'], dict):
            return {
                'result': list(self._query_defined_entities(_req['name'], _req['props']).values())
            }

        else:
            return {
                'result': list(self._query_entities(_req['name']).values())
            }

    def get_mails(self, _req):
        return {
            'result': self.event_store.get('mail') or []
        }

    def get_unbilled_orders(self, _req):
        return {
            'result': self._unbilled_orders()
        }

    def get_unshipped_orders(self, _req):
        return {
            'result': self._unshipped_orders()
        }

    def get_delivered_orders(self, _req):
        return {
            'result': self._delivered_orders()
        }
예제 #5
0
class ProductService(object):
    """
    Product Service class.
    """
    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers(
            'product-service',
            [self.create_products, self.update_product, self.delete_product])

    @staticmethod
    def _create_entity(_name, _price):
        """
        Create a product entity.

        :param _name: The name of the product.
        :param _price: The price of the product.
        :return: A dict with the entity properties.
        """
        return {'entity_id': str(uuid.uuid4()), 'name': _name, 'price': _price}

    def start(self):
        logging.info('starting ...')
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.consumers.stop()
        logging.info('stopped.')

    def create_products(self, _req):
        products = _req if isinstance(_req, list) else [_req]
        product_ids = []

        for product in products:
            try:
                new_product = ProductService._create_entity(
                    product['name'], product['price'])
            except KeyError:
                return {
                    "error":
                    "missing mandatory parameter 'name' and/or 'price'"
                }

            # trigger event
            self.event_store.publish(
                'product', create_event('entity_created', new_product))

            product_ids.append(new_product['entity_id'])

        return {"result": product_ids}

    def update_product(self, _req):
        try:
            product_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'product',
            'id': product_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        product = rsp['result']
        if not product:
            return {"error": "could not find product"}

        # set new props
        product['entity_id'] = product_id
        try:
            product['name'] = _req['name']
            product['price'] = _req['price']
        except KeyError:
            return {
                "result": "missing mandatory parameter 'name' and/or 'price"
            }

        # trigger event
        self.event_store.publish('product',
                                 create_event('entity_updated', product))

        return {"result": True}

    def delete_product(self, _req):
        try:
            product_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'product',
            'id': product_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        product = rsp['result']
        if not product:
            return {"error": "could not find product"}

        # trigger event
        self.event_store.publish('product',
                                 create_event('entity_deleted', product))

        return {"result": True}
예제 #6
0
class OrderService(object):
    """
    Order Service class.
    """

    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers('order-service', [self.create_orders,
                                                     self.update_order,
                                                     self.delete_order])

    @staticmethod
    def _create_entity(_cart_id, _status='CREATED'):
        """
        Create an order entity.

        :param _cart_id: The cart ID the order is for.
        :param _status: The current status of the order, defaults to CREATED.
                        Other options are OUT_OF_STOCK, IN_STOCK, CLEARED, UNCLEARED, SHIPPED and DELIVERED.
        :return: A dict with the entity properties.
        """
        return {
            'entity_id': str(uuid.uuid4()),
            'cart_id': _cart_id,
            'status': _status,
        }

    def start(self):
        logging.info('starting ...')
        self.event_store.subscribe('billing', self.billing_created)
        self.event_store.subscribe('billing', self.billing_deleted)
        self.event_store.subscribe('shipping', self.shipping_created)
        self.event_store.subscribe('shipping', self.shipping_updated)
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.event_store.unsubscribe('billing', self.billing_created)
        self.event_store.unsubscribe('billing', self.billing_deleted)
        self.event_store.unsubscribe('shipping', self.shipping_created)
        self.event_store.unsubscribe('shipping', self.shipping_updated)
        self.consumers.stop()
        logging.info('stopped.')

    def create_orders(self, _req):
        orders = _req if isinstance(_req, list) else [_req]
        order_ids = []

        for order in orders:
            try:
                new_order = OrderService._create_entity(order['cart_id'])
            except KeyError:
                return {
                    "error": "missing mandatory parameter 'cart_id'"
                }

            # trigger event
            self.event_store.publish('order', create_event('entity_created', new_order))

            order_ids.append(new_order['entity_id'])

        return {
            "result": order_ids
        }

    def update_order(self, _req):
        try:
            order_id = _req['entity_id']
        except KeyError:
            return {
                "error": "missing mandatory parameter 'entity_id'"
            }

        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': order_id})
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        order = rsp['result']
        if not order:
            return {
                "error": "could not find order"
            }

        # set new props
        order['entity_id'] = order_id
        try:
            order['cart_id'] = _req['cart_id']
            order['status'] = _req['status']
        except KeyError:
            return {
                "result": "missing mandatory parameter 'cart_id' and/or 'status"
            }

        # trigger event
        self.event_store.publish('order', create_event('entity_updated', order))

        return {
            "result": True
        }

    def delete_order(self, _req):
        try:
            order_id = _req['entity_id']
        except KeyError:
            return {
                "error": "missing mandatory parameter 'entity_id'"
            }

        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': order_id})
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        order = rsp['result']
        if not order:
            return {
                "error": "could not find order"
            }

        # trigger event
        self.event_store.publish('order', create_event('entity_deleted', order))

        return {
            "result": True
        }

    def billing_created(self, _item):
        if _item.event_action != 'entity_created':
            return

        billing = json.loads(_item.event_data)
        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': billing['order_id']})
        order = rsp['result']
        if not order['status'] == 'IN_STOCK':
            return

        order['status'] = 'CLEARED'
        self.event_store.publish('order', create_event('entity_updated', order))

    def billing_deleted(self, _item):
        if _item.event_action != 'entity_delted':
            return

        billing = json.loads(_item.event_data)
        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': billing['order_id']})
        order = rsp['result']
        if not order['status'] == 'CLEARED':
            return

        order['status'] = 'UNCLEARED'
        self.event_store.publish('order', create_event('entity_updated', order))

    def shipping_created(self, _item):
        if _item.event_action != 'entity_created':
            return

        shipping = json.loads(_item.event_data)
        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': shipping['order_id']})
        order = rsp['result']
        if not order['status'] == 'CLEARED':
            return

        order['status'] = 'SHIPPED'
        self.event_store.publish('order', create_event('entity_updated', order))

    def shipping_updated(self, _item):
        if _item.event_action != 'entity_updated':
            return

        shipping = json.loads(_item.event_data)
        if not shipping['delivered']:
            return

        rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': shipping['order_id']})
        order = rsp['result']
        order['status'] = 'DELIVERED'
        self.event_store.publish('order', create_event('entity_updated', order))
class ShippingService(object):
    """
    Shipping Service class.
    """
    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers('shipping-service', [
            self.create_shippings, self.update_shipping, self.delete_shipping
        ])

    @staticmethod
    def _create_entity(_order_id, _delivered=0):
        """
        Create a shipping entity.

        :param _order_id: The order which is shipped.
        :param _delivered: Optional TS indicating delivery, defaults to 0 (not delivered).
        :return: A dict with the entity properties.
        """
        return {
            'entity_id': str(uuid.uuid4()),
            'order_id': _order_id,
            'delivered': _delivered
        }

    def start(self):
        logging.info('starting ...')
        self.event_store.subscribe('billing', self.billing_created)
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.event_store.unsubscribe('billing', self.billing_created)
        self.consumers.stop()
        logging.info('stopped.')

    def create_shippings(self, _req):
        shippings = _req if isinstance(_req, list) else [_req]
        shipping_ids = []

        for shipping in shippings:
            try:
                new_shipping = ShippingService._create_entity(
                    shipping['order_id'])
            except KeyError:
                return {
                    "error":
                    "missing mandatory parameter 'order_id' and/or 'delivered'"
                }

            # trigger event
            self.event_store.publish(
                'shipping', create_event('entity_created', new_shipping))

            shipping_ids.append(new_shipping['entity_id'])

        return {"result": shipping_ids}

    def update_shipping(self, _req):
        try:
            shipping_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'shipping',
            'id': shipping_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        shipping = rsp['result']
        if not shipping:
            return {"error": "could not find shipping"}

        # set new props
        shipping['entity_id'] = shipping_id
        try:
            shipping['order_id'] = _req['order_id']
            shipping['delivered'] = _req['delivered']
        except KeyError:
            return {
                "result":
                "missing mandatory parameter 'order_id' and/or 'delivered"
            }

        # trigger event
        self.event_store.publish('shipping',
                                 create_event('entity_updated', shipping))

        return {"result": True}

    def delete_shipping(self, _req):
        try:
            shipping_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'shipping',
            'id': shipping_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        shipping = rsp['result']
        if not shipping:
            return {"error": "could not find shipping"}

        # trigger event
        self.event_store.publish('shipping',
                                 create_event('entity_deleted', shipping))

        return {"result": True}

    def billing_created(self, _item):
        if _item.event_action != 'entity_created':
            return

        billing = json.loads(_item.event_data)
        shipping = ShippingService._create_entity(billing['order_id'])
        self.event_store.publish('shipping',
                                 create_event('entity_created', shipping))
class InventoryService(object):
    """
    Inventory Service class.
    """
    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers('inventory-service', [
            self.create_inventories, self.update_inventory,
            self.delete_inventory
        ])

    @staticmethod
    def _create_entity(_product_id, _amount):
        """
        Create an inventory entity.

        :param _product_id: The product ID the inventory is for.
        :param _amount: The amount of products in the inventory.
        :return: A dict with the entity properties.
        """
        return {
            'entity_id': str(uuid.uuid4()),
            'product_id': _product_id,
            'amount': _amount
        }

    def _incr_inventory(self, _product_id, _value=1):
        rsp = send_message('read-model', 'get_entity', {
            'name': 'inventory',
            'props': {
                'product_id': _product_id
            }
        })
        if 'error' in rsp:
            raise Exception(rsp['error'] + ' (from read-model)')

        inventory = rsp['result']
        if not inventory:
            logging.error(
                "could not find inventory for product {}".format(_product_id))
            return False

        inventory['amount'] = int(
            inventory['amount']) - (_value if _value else 1)

        # trigger event
        self.event_store.publish('inventory',
                                 create_event('entity_updated', inventory))

        return True

    def _decr_inventory(self, _product_id, _value=1):
        rsp = send_message('read-model', 'get_entity', {
            'name': 'inventory',
            'props': {
                'product_id': _product_id
            }
        })
        if 'error' in rsp:
            raise Exception(rsp['error'] + ' (from read-model)')

        inventory = rsp['result']
        if not inventory:
            logging.warning(
                "could not find inventory for product {}".format(_product_id))
            return False

        if int(inventory['amount']) - (_value if _value else 1) < 0:
            logging.info("product {} is out of stock".format(_product_id))
            return False

        inventory['amount'] = int(
            inventory['amount']) - (_value if _value else 1)

        # trigger event
        self.event_store.publish('inventory',
                                 create_event('entity_updated', inventory))

        return True

    def _decr_from_cart(self, _cart):
        try:
            product_ids = _cart['product_ids']
        except KeyError:
            raise Exception("missing mandatory parameter 'product_ids'")

        rsp = send_message('read-model', 'get_entities', {
            'name': 'inventory',
            'props': {
                'product_id': product_ids
            }
        })
        if 'error' in rsp:
            raise Exception(rsp['error'] + ' (from read-model)')

        inventories = rsp['result']

        # count products
        product_counts = []
        for inventory in inventories:
            found = product_ids.count(inventory['product_id'])

            # check amount
            if found > int(inventory['amount']):
                logging.info("product {} is out of stock".format(
                    inventory['product_id']))
                return False

            product_counts.append((inventory, found))

        # decrement inventory
        for inventory, count in product_counts:
            inventory['amount'] = int(inventory['amount']) - count

            # trigger event
            self.event_store.publish('inventory',
                                     create_event('entity_updated', inventory))

        return True

    def start(self):
        logging.info('starting ...')
        self.event_store.subscribe('order', self.order_created)
        self.event_store.subscribe('order', self.order_deleted)
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.event_store.unsubscribe('order', self.order_created)
        self.event_store.unsubscribe('order', self.order_deleted)
        self.consumers.stop()
        logging.info('stopped.')

    def create_inventories(self, _req):
        inventory = _req if isinstance(_req, list) else [_req]
        inventory_ids = []

        for inventory in inventory:
            try:
                new_inventory = InventoryService._create_entity(
                    inventory['product_id'], inventory['amount'])
            except KeyError:
                return {
                    "error":
                    "missing mandatory parameter 'product_id' and/or 'amount'"
                }

            # trigger event
            self.event_store.publish(
                'inventory', create_event('entity_created', new_inventory))

            inventory_ids.append(new_inventory['entity_id'])

        return {"result": inventory_ids}

    def update_inventory(self, _req):
        try:
            inventory_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'inventory',
            'id': inventory_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        inventory = rsp['result']
        if not inventory:
            return {"error": "could not find inventory"}

        # set new props
        inventory['entity_id'] = inventory_id
        try:
            inventory['product_id'] = _req['product_id']
            inventory['amount'] = _req['amount']
        except KeyError:
            return {
                "result":
                "missing mandatory parameter 'product_id' and/or 'amount"
            }

        # trigger event
        self.event_store.publish('inventory',
                                 create_event('entity_updated', inventory))

        return {"result": True}

    def delete_inventory(self, _req):
        try:
            inventory_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'inventory',
            'id': inventory_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        inventory = rsp['result']
        if not inventory:
            return {"error": "could not find inventory"}

        # trigger event
        self.event_store.publish('inventory',
                                 create_event('entity_deleted', inventory))

        return {"result": True}

    def order_created(self, _item):
        if _item.event_action != 'entity_created':
            return

        order = json.loads(_item.event_data)
        rsp = send_message('read-model', 'get_entity', {
            'name': 'cart',
            'id': order['cart_id']
        })
        cart = rsp['result']
        result = self._decr_from_cart(cart)
        order['status'] = 'IN_STOCK' if result else 'OUT_OF_STOCK'
        self.event_store.publish('order', create_event('entity_updated',
                                                       order))

    def order_deleted(self, _item):
        if _item.event_action != 'entity_deleted':
            return

        order = json.loads(_item.event_data)
        if order['status'] != 'IN_STOCK':
            return

        rsp = send_message('read-model', 'get_entity', {
            'name': 'cart',
            'id': order['cart_id']
        })
        cart = rsp['result']
        [
            self._incr_inventory(product_id)
            for product_id in cart['product_ids']
        ]
예제 #9
0
class CartService(object):
    """
    Cart Service class.
    """
    def __init__(self):
        self.event_store = EventStoreClient()
        self.consumers = Consumers(
            'cart-service',
            [self.create_carts, self.update_cart, self.delete_cart])

    @staticmethod
    def _create_entity(_customer_id, _product_ids):
        """
        Create a cart entity.

        :param _customer_id: The customer ID.
        :param _product_ids: The product IDs.
        :return: A dict with the entity properties.
        """
        return {
            'entity_id': str(uuid.uuid4()),
            'customer_id': _customer_id,
            'product_ids': _product_ids
        }

    @staticmethod
    def _check_inventory(_product_ids):
        product_counts = collections.Counter(_product_ids)
        for product_id, amount in product_counts.items():
            rsp = send_message('read-model', 'get_entity', {
                'name': 'inventory',
                'props': {
                    'product_id': product_id
                }
            })
            if 'error' in rsp:
                rsp['error'] += ' (from read-model)'
                raise Exception(rsp['error'])

            inventory = rsp['result']
            if not inventory or int(inventory['amount']) - amount < 0:
                return False, product_id

        return True, None

    def start(self):
        logging.info('starting ...')
        self.consumers.start()
        self.consumers.wait()

    def stop(self):
        self.consumers.stop()
        logging.info('stopped.')

    def create_carts(self, _req):
        carts = _req if isinstance(_req, list) else [_req]
        cart_ids = []

        for cart in carts:
            res, product_id = self._check_inventory(cart['product_ids'])
            if not res:
                return {
                    'error': 'product {} is out of stock'.format(product_id)
                }

            try:
                new_cart = CartService._create_entity(cart['customer_id'],
                                                      cart['product_ids'])
            except KeyError:
                return {
                    "error":
                    "missing mandatory parameter 'customer_id' and/or 'product_ids'"
                }

            # trigger event
            self.event_store.publish('cart',
                                     create_event('entity_created', new_cart))

            cart_ids.append(new_cart['entity_id'])

        return {"result": cart_ids}

    def update_cart(self, _req):
        try:
            cart_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'order',
            'props': {
                'cart_id': cart_id
            }
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        order = rsp['result']
        if order and not order['status'] == 'CREATED':
            return {"error": "order {} in progress".format(order['entity_id'])}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'cart',
            'id': cart_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        cart = rsp['result']
        if not cart:
            return {"error": "could not find cart {}".format(cart_id)}

        # set new props
        cart['entity_id'] = cart_id
        try:
            cart['customer_id'] = _req['customer_id']
            cart['product_ids'] = _req['product_ids']
        except KeyError:
            return {
                "result":
                "missing mandatory parameter 'customer_id' and/or 'product_ids"
            }

        res, product_id = self._check_inventory(cart['product_ids'])
        if not res:
            return {'error': 'product {} is out of stock'.format(product_id)}

        # trigger event
        self.event_store.publish('cart', create_event('entity_updated', cart))

        return {"result": True}

    def delete_cart(self, _req):
        try:
            cart_id = _req['entity_id']
        except KeyError:
            return {"error": "missing mandatory parameter 'entity_id'"}

        rsp = send_message('read-model', 'get_entity', {
            'name': 'cart',
            'id': cart_id
        })
        if 'error' in rsp:
            rsp['error'] += ' (from read-model)'
            return rsp

        cart = rsp['result']
        if not cart:
            return {"error": "could not find cart"}

        # trigger event
        self.event_store.publish('cart', create_event('entity_deleted', cart))

        return {"result": True}