Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bitcoinstore/inventory/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class ItemNotFoundException(Exception):
pass


class InsufficientInventoryException(Exception):
pass


class AlreadyExistingInventoryException(Exception):
pass
14 changes: 14 additions & 0 deletions bitcoinstore/inventory/fungible_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

class FungibleItem:
"""
A class object to hold contents of a row of fungible_inventory database.
This is the object returned from the DAO, and to clients of the InventoryService.
"""
def __init__(self, sku = '', name = '', description = '', price_cents = 0, shipping_weight = 0, quantity = 0):
self.sku = sku
self.name = name
self.description = description
self.price_cents = price_cents
self.shipping_weight = shipping_weight
self.quantity = quantity

150 changes: 150 additions & 0 deletions bitcoinstore/inventory/inventory_dao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, text

from bitcoinstore.inventory.fungible_item import FungibleItem
from bitcoinstore.inventory.non_fungible_item import NonFungibleItem

engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True)
meta = MetaData()

fungible_inventory = Table(
'fungible_inventory', meta,
Column('sku', String, primary_key=True),
Column('name', String),
Column('description', String),
Column('price', Integer),
Column('shipping_weight', Integer),
Column('quantity', Integer)
)

non_fungible_inventory = Table(
'non_fungible_inventory', meta,
Column('sku', String, primary_key=True),
Column('serial', String, primary_key=True),
Column('name', String),
Column('description', String),
Column('unique_description', String),
Column('price', Integer),
Column('shipping_weight', Integer),
Column('is_available', Boolean)
)

"""
*
*
*
For the purpose of testing, below will create the tables, and insert some static rows.
*
*
*
"""

meta.create_all(engine)

with engine.begin() as conn:
conn.execute(
fungible_inventory.insert(), [
{'sku': 'A', 'name': 'name of A', 'description': 'Description of A',
'price': 300, 'shipping_weight': '142', 'quantity': 5},
{'sku': 'B', 'name': 'name of B', 'description': 'Description of B',
'price': 400, 'shipping_weight': '152', 'quantity': 8},
{'sku': 'C', 'name': 'name of C', 'description': 'Description of C',
'price': 100, 'shipping_weight': '12', 'quantity': 0}
]
)
conn.execute(
non_fungible_inventory.insert(), [
{'sku': 'D', 'serial': 'D1', 'name': 'name of D', 'description': 'Description of D',
'unique_description': 'D1 of D', 'price': 300, 'shipping_weight': '142', 'is_available': True},
{'sku': 'E', 'serial': 'E1', 'name': 'name of E', 'description': 'Description of E',
'unique_description': 'E1 of E', 'price': 330, 'shipping_weight': '13', 'is_available': False}
]
)


class FungibleInventoryDao:
"""
DAO for the table fungible_inventory
"""

def __init__(self):
self._conn = engine.connect()

def list_items(self):
ex = fungible_inventory.select()
rows = self._conn.execute(ex)
items = []
for row in rows:
sku, name, description, price, shipping_weight, quantity = row
item = FungibleItem(sku, name, description, price, shipping_weight, quantity)
items.append(item)
return items

def get_item(self, sku):
t = text("SELECT * FROM non_fungible_inventory WHERE SKU = :sku")
rows = self._conn.execute(t, {"sku": sku}).fetchall()
assert (len(rows) < 2), "Multiple items for given SKU!"
if len(rows) == 0:
return None
sku, name, description, price, shipping_weight, quantity = rows[0]
return FungibleItem(sku, name, description, price, shipping_weight, quantity)

def update_item_count(self, sku, count):
t = text("UPDATE fungible_inventory SET quantity = :count WHERE SKU = :sku")
self._conn.execute(t, count=count, sku=sku)

def create_item(self, fungible_item):
t = text("INSERT INTO fungible_inventory (sku, name, description, price, shipping_weight, quantity) "
"VALUES (:sku, :name, :desc, :price, shipping_wt, :qty)")
self._conn.execute(t, sku=fungible_item.sku, name=fungible_item.name, desc=fungible_item.description,
price=fungible_item.price, shipping_wt=fungible_item.shipping_weight,
qty=fungible_item.quantity)

def delete_item(self, sku):
t = text("DELETE FROM fungible_inventory WHERE SKU = :sku")
self._conn.execute(t, sku=sku)


class NonFungibleInventoryDao:
"""
DAO for the table non_fungible_inventory
"""

def __init__(self):
self._conn = engine.connect()

def list_items(self):
query = non_fungible_inventory.select()
rows = self._conn.execute(query)
items = []
for row in rows:
sku, serial, name, description, unique_description, price, shipping_weight, is_available = row
item = NonFungibleItem(sku, serial, name, description, unique_description, price, shipping_weight,
is_available)
items.append(item)
return items

def get_item(self, sku, serial):
t = text("select * from non_fungible_inventory where SKU = :sku and serial = :serial")
rows = self._conn.execute(t, sku=sku, serial=serial).fetchall()
assert (len(rows) < 2), "Multiple items for given SKU and Serial number!"
if len(rows) == 0:
return None
sku, serial, name, description, unique_description, price, shipping_weight, is_available = rows[0]
return FungibleItem(sku, serial, name, description, unique_description, price, shipping_weight, is_available)

def update_item_availability(self, sku, serial, is_available):
t = text("update non_fungible_inventory set is_available = :availability where SKU = :sku and serial = :serial")
self._conn.execute(t, availability=is_available, sku=sku, serial=serial)

def create_item(self, non_fungible_item):
t = text("INSERT INTO non_fungible_inventory (sku, serial, name, description, unique_description, price, "
"shipping_weight, quantity) VALUES (:sku, :serial, :name, :desc, :unique_desc, :price, shipping_wt, "
":qty)")
self._conn.execute(t, sku=non_fungible_item.sku, serial=non_fungible_item.serial_number,
name=non_fungible_item.name, desc=non_fungible_item.description,
unique_desc=non_fungible_item.unique_description, price=non_fungible_item.price,
shipping_wt=non_fungible_item.shipping_weight, qty=non_fungible_item.quantity)

def delete_item(self, sku, serial):
t = text("DELETE FROM non_fungible_inventory WHERE SKU = :sku and serial = :serial")
self._conn.execute(t, sku=sku, serial=serial)
161 changes: 161 additions & 0 deletions bitcoinstore/inventory/inventory_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from bitcoinstore.inventory.inventory_dao import FungibleInventoryDao, NonFungibleInventoryDao
from threading import Lock
from bitcoinstore.inventory.exceptions import *


class InventoryService:
"""
Implementation for the Inventory Service.
This is to be called by the main browser frontend, Cart Service, Reservation Timeout Serivce, and backend inventory
management service.

Note that this service is NOT aware of how the reservation system works. Whenever an item is added to a cart, that
essentially removes an inventory item from this service. And when item is removed from cart, manually or via a
timeout system, the item is added back to the inventory via this service. The difference between reserved items, vs
fulfilled items is maintained by the Reservation Service, outside of Inventory Service.

Note that Inventory Service is not directly handling client requests, so it doesn't handle authorization for the
user context. It is assumed higher level services will do those checks.

This class is thread safe.
"""

# (TODO): The locks below are not granular currently. Ideally replace them by named resource locks, which can
# lock on a unique key like SKU or serial. This way, a thread will only lock the DB rows that it needs to touch.
_fungible_lock = Lock()
_non__fungible_lock = Lock()

def __init__(self):
self._fungible_dao = FungibleInventoryDao()
self._non_fungible_dao = NonFungibleInventoryDao()

def list_fungible_items(self):
"""
Returns list of all fungible items.
Doesn't require acquiring a lock.
"""
return self._fungible_dao.list_items()

def get_fungible_item(self, sku):
"""
Returns a particular fungible item identified by the sku nmme.
Doesn't require acquiring a lock.
"""
return self._fungible_dao.get_item(sku)

def remove_fungible_item(self, sku, count):
"""
Removes few items from a particular fungible item identified by the sku nmme.
Returns errors if item doesn't exist or short of inventory.

Note that if removal of items reduced invetory size to 0, this DOES NOT delete the item from inventory.

It requires acquiring a lock, since we are adjusting inventory quantity over 2 different db calls.
"""

self._fungible_lock.acquire()
item = self.get_fungible_item(sku)
if item is None:
self._fungible_lock.release()
raise ItemNotFoundException()
if item.quantity < count:
self._fungible_lock.release()
raise InsufficientInventoryException()
new_count = item.quantity - count
self._fungible_dao.update_item_count(sku, new_count)
self._fungible_lock.release()

def add_fungible_item(self, sku, count):
"""
Adds a few items(count) to a particular fungible item identified by the sku nmme.
Returns errors if the inventory item doesn't exist.

It requires acquiring a lock, since we are adjusting inventory quantity over 2 different db calls.
"""
self._fungible_lock.acquire()
item = self.get_fungible_item(sku)
if item is None:
self._fungible_lock.release()
raise ItemNotFoundException()
new_count = item.quantity + count
self._fungible_dao.update_item_count(sku, new_count)
self._fungible_lock.release()

def create_new_fungible_item(self, fungible_item):
"""
Creates a new fungible item.
Returns error if the inventory item already exist.

It requires acquiring a lock, since we want only the winning thread in a race condition to return success. The
losing thread in the race condition will get an error, indicating its version failed to apply.
"""
self._fungible_lock.acquire()
item = self.get_fungible_item(fungible_item.sku)
if item is not None:
self._fungible_lock.release()
raise AlreadyExistingInventoryException()
self._fungible_dao.create_item(fungible_item)
self._fungible_lock.release()

def delete_fungible_item(self, sku):
"""
Deletes a fungible item, identified by the sku.
Throws error if item didn't exist.

Doesn't require a lock.
"""
item = self.get_fungible_item(sku)
if item is None:
raise ItemNotFoundException()
self._fungible_dao.delete_item(sku)

"""
All methods below are for non-fungible inventory, and implemented similarly to the fungible inventory, except for
minor details on the quantity vs is_available columns.
The documentation for below methods should be almost same as for above methods for fungible inventory.
"""

def list_non_fungible_items(self):
return self._non_fungible_dao.list_items()

def get_non_fungible_item(self, sku, serial):
return self._non_fungible_dao.get_item(sku, serial)

def remove_non_fungible_item(self, sku, serial):
self._non_fungible_lock.acquire()
item = self.get_non_fungible_item(sku, serial)
if item is None:
self._fungible_lock.release()
raise ItemNotFoundException()
if not item.is_available:
self._fungible_lock.release()
raise InsufficientInventoryException()
self._non_fungible_dao.update_item_availability(sku, serial, False)
self._non_fungible_lock.release()

def add_non_fungible_item(self, sku, serial):
self._non_fungible_lock.acquire()
item = self.get_non_fungible_item(sku, serial)
if item is None:
self._fungible_lock.release()
raise ItemNotFoundException()
if item.is_available:
self._fungible_lock.release()
raise AlreadyExistingInventoryException()
self._non_fungible_dao.update_item_availability(sku, serial, True)
self._non_fungible_lock.release()

def create_new_non_fungible_item(self, non_fungible_item):
self._non_fungible_lock.acquire()
item = self.get_non_fungible_item(non_fungible_item.sku, non_fungible_item.serial)
if item is not None:
self._fungible_lock.release()
raise AlreadyExistingInventoryException()
self._non_fungible_dao.create_item(non_fungible_item)
self._non_fungible_lock.release()

def delete_non_fungible_item(self, sku, serial):
item = self.get_non_fungible_item(sku, serial)
if item is None:
raise ItemNotFoundException()
self._non_fungible_dao.delete_item(sku, serial)
17 changes: 17 additions & 0 deletions bitcoinstore/inventory/non_fungible_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

class NonFungibleItem:
"""
A class object to hold contents of a row of non_fungible_inventory database.
This is the object returned from the DAO, and to clients of the InventoryService.
"""

def __init__(self, sku = '', serial_number = '', name = '', description = '', unique_description = '',
price_cents = 0, shipping_weight = 0, is_available = False):
self.sku = sku
self.serial_number = serial_number
self.name = name
self.description = description
self.unique_description = unique_description
self.price_cents = price_cents
self.shipping_weight = shipping_weight
self.is_available = is_available
26 changes: 26 additions & 0 deletions test/bitcoinstore/inventory/test_functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

import pytest

from bitcoinstore.inventory.exceptions import ItemNotFoundException, InsufficientInventoryException, \
AlreadyExistingInventoryException
from bitcoinstore.inventory.fungible_item import FungibleItem
from bitcoinstore.inventory.inventory_dao import FungibleInventoryDao
from bitcoinstore.inventory.inventory_service import InventoryService

class TestFunctionalInventoryService():

@pytest.fixture(autouse=True)
def setup(self):
self.service = InventoryService()

def test_list_fungible_items(self):
items = self.service.list_fungible_items()
assert len(items) == 3
for item in items:
assert item.sku is not None

def test_get_fungible_item(self):
sku = 'A'
item = self.service.get_fungible_item(sku)

# (TODO) Finish the test, and other functional tests.
Loading