Chapterبەش 6 · Lectureوانە 6.1
Course Project: Ship Bazaar Cart 1.0.0پڕۆژەی خولەکە: Bazaar Cart 1.0.0 بڵاو بکەرەوە
A guided capstone that puts every lesson of the course to work: six user stories with acceptance examples and MoSCoW, two sprints on a Kanban board, a Git repository with a feature branch and a resolved merge conflict, a layered design with MVP and dependency injection, tests written first for rules R1–R5, release 1.0.0 with a CHANGELOG, an annotated tag and a zipapp build, then a bug report with a regression test (1.0.1) and a new feature (1.1.0) — with checkpoints, a marking checklist and worked answers.
پڕۆژەیەکی کۆتایی بە ڕێنمایی کە هەموو وانەکانی خولەکە دەخاتە کار: شەش بە نموونەی پەسەندکردن و MoSCoW، دوو لەسەر تەختەی ، Git بە و چارەسەرکراو، دیزاینێکی چینچین بە MVP و ، تاقیکردنەوە لە پێشەوە بۆ یاساکانی R1–R5، 1.0.0 بە CHANGELOG، تاگی annotated و بیلدی zipapp، پاشان ڕاپۆرتی بە (1.0.1) و تایبەتمەندییەکی نوێ (1.1.0) — لەگەڵ خاڵی پشکنین، لیستی نمرەدان و وەڵامی ڕوونکراوە.
What you will learnچی فێر دەبیت
- Turn a shop's needs into user stories with acceptance examples, non-functional requirements and a MoSCoW plan.
- Keep a clean Git history with a feature branch, a resolved conflict and annotated release tags.
- Build a layered Python app with MVP and dependency injection, tests first.
- Release 1.0.0, a patch and a minor version with semantic versioning, a changelog and a zipapp build.
- Handle a bug report with a regression test, and judge a project with a marking checklist and a retrospective.
- پێویستییەکانی دوکانێک بکەیتە بە نموونەی پەسەندکردن، و پلانێکی MoSCoW.
- مێژوویەکی پاکی Git بهێڵیتەوە بە ، ناکۆکییەکی چارەسەرکراو و تاگی annotated بۆ .
- Pythonـی چینچین بە MVP و دروست بکەیت، تاقیکردنەوە لە پێشەوە.
- 1.0.0، و بە ، و بیلدی zipapp بڵاو بکەیتەوە.
- ڕاپۆرتی بە چارەسەر بکەیت و بە لیستی نمرەدان و ئاوڕدانەوە هەڵسەنگاندن بۆ پڕۆژەیەک بکەیت.
The brief: ship Bazaar Cartئەرکەکە: Bazaar Cart بڵاو بکەرەوە
This project uses the whole course. You build a small but complete Bazaar Cart from an empty folder: a console app for Soran's dried-fruit and spice shop in the Sulaimani bazaar (made-up prices in IQD). You plan it, keep its history in Git, test it, organise it in layers and release it — 1.0.0, then a bug fix 1.0.1 and a new feature 1.1.0. Nothing here is new: every step links the lesson that teaches it.
ئەم پڕۆژەیە هەموو خولەکە بەکار دەهێنێت. Bazaar Cartـێکی بچووک بەڵام تەواو لە بەتاڵەوە دروست دەکەیت: کۆنسۆڵ بۆ دوکانی میوەی وشک و بەهاراتی سۆران لە بازاڕی سلێمانی (نرخەکان دروستکراون، بە دینار). پلانی بۆ دادەنێیت، مێژووەکەی لە Git هەڵدەگریت، تاقیی دەکەیتەوە، بە ڕێکی دەخەیت و بڵاوی دەکەیتەوە — 1.0.0، پاشان چاکسازییەکی بە 1.0.1 و تایبەتمەندییەکی نوێ بە 1.1.0. هیچ شتێک لێرەدا نوێ نییە: هەر هەنگاوێک بەستەری ئەو وانەیەی هەیە کە فێری دەکات.
- Work alone or in a team of 3–4 (Aram leads and owns the product, Shilan and Karwan write code, Dilan tests). The worked run below is done by one person on one computer; in a team each person works in a clone and opens pull requests (GitHub and teamwork).
- Keep the repository private and use no real customer data: the phone numbers are fake (
0770 000 0001) and no secret key ever enters Git. - Hand in: the repository with its history and tags; README and CHANGELOG; a test suite that passes; the
.pyzbuild; a one-page plan (stories, MoSCoW, board); and a short retrospective.
- بە تەنیا کار بکە یان لە تیمێکی 3–4 کەسیدا (ئارام سەرپەرشتی دەکات و ، شیلان و کاروان کۆد دەنووسن، دیلان تاقی دەکاتەوە). ئەو جێبەجێکردنە نموونەییەی خوارەوە یەک کەس لەسەر یەک کردوویەتی؛ لە تیمێکدا هەر کەسێک لە کار دەکات و دەکاتەوە (GitHub و کاری تیمی).
- تایبەت ڕابگرە و هیچ ڕاستەقینەی کڕیار بەکار مەهێنە: ژمارە تەلەفۆنەکان ساختەن (
0770 000 0001) و هیچ کلیلێکی نهێنی هەرگیز ناچێتە ناو Gitـەوە. - ئەمانە ڕادەست بکە: لەگەڵ مێژوو و ؛ README و CHANGELOG؛ کە سەردەکەوێت؛
.pyz؛ پلانێکی یەک لاپەڕەیی (چیرۆکەکان، MoSCoW، تەختە)؛ و ئاوڕدانەوەیەکی کورت.
Step 1: stories and requirementsهەنگاوی 1: چیرۆکەکان و پێداویستییەکان
Start with what Soran needs, not with code (requirements and design). Write each need as a user story with acceptance criteria — real numbers that a test can check later — and rank the stories with MoSCoW. The delivery story keeps the label it has in the requirements lesson, BC-7.
بە ئەوە دەست پێبکە کە سۆران پێویستیەتی، نەک بە کۆد (پێداویستییەکان و دیزاین). هەر پێویستییەک وەک لەگەڵ بنووسە — ژمارەی ڕاستەقینە کە دواتر تاقیکردنەوەیەک بتوانێت بیپشکنێت — و بە ڕیزبەندیی چیرۆکەکان بکە. چیرۆکی گەیاندن هەمان ناونیشانی وانەی دەهێڵێتەوە: BC-7.
| Storyچیرۆک | As a … I want … so that …وەک … دەمەوێت … بۆ ئەوەی … | Acceptance examplesنموونەکانی پەسەندکردن | MoSCoW |
|---|---|---|---|
| BC-1 | As a customer, I want each line priced, so that I know what I pay (R1, R2).وەک کڕیار، دەمەوێت نرخی هەر دێڕێک ببینم، بۆ ئەوەی بزانم چی دەدەم (R1، R2). | walnuts 500 g → 9,000; sumac 350 g → 3,000; 99 g and 10,050 g refusedگوێز 500 گرام ← 9,000؛ سوماق 350 گرام ← 3,000؛ 99 گرام و 10,050 گرام ڕەت دەکرێنەوە | Must |
| BC-2 | As Soran, I want orders never to take more than my stock (R3).وەک سۆران، دەمەوێت هیچ داواکارییەک لە کۆگاکەم زیاتر نەبات (R3). | honey 3,500 g refused when 3,000 g are in stockهەنگوین 3,500 گرام ڕەت دەکرێتەوە کاتێک 3,000 گرام لە کۆگادایە | Must |
| BC-3 | As a customer, I want to build my cart line by line and confirm it.وەک کڕیار، دەمەوێت سەبەتەکەم دێڕ بە دێڕ دروست بکەم و پشتڕاستی بکەمەوە. | "walnuts 500" shows the line and the total; letters for grams show an error«walnuts 500» دێڕەکە و کۆکە پیشان دەدات؛ پیت لەبری گرام پیشان دەدات | Must |
| BC-7 | As a customer, I want to see the delivery fee before I order (R4).وەک کڕیار، دەمەوێت پێش داواکردن کرێی گەیاندن ببینم (R4). | goods 49,750 → fee 3,000; goods 50,000 → freeکاڵای 49,750 ← کرێ 3,000؛ کاڵای 50,000 ← بێبەرانبەر | Must |
| BC-4 | As a customer, I want an SMS when my order is placed.وەک کڕیار، دەمەوێت کاتێک داواکارییەکەم تۆمار دەکرێت وەربگرم. | exactly one SMS with the total; none when a rule says noڕێک یەک نامە بە کۆکەوە؛ هیچ نامەیەک کاتێک یاسایەک ڕەتی دەکاتەوە | Should |
| BC-8 | As a regular customer, I want 5 % off big orders (R5).وەک کڕیاری بەردەوام، دەمەوێت 5 لە سەد داشکاندن بۆ داواکاریی گەورە (R5). | goods 100,000 → 5,000 off; 99,750 → none (planned for 1.1.0)کاڵای 100,000 ← 5,000 داشکاندن؛ 99,750 ← هیچ (بۆ 1.1.0 پلان کراوە) | Could |
Three non-functional requirements: N1 Python 3.13 and its standard library only, shipped as one .pyz file; N2 the whole test suite runs in under 5 seconds; N3 no real customer data, and secrets never enter the repository. Won't have this time: card payment, delivery outside Sulaimani and Soran's list of new orders.
سێ : N1 تەنیا Python 3.13 و ، وەک یەک فایلی .pyz؛ N2 هەموو لە کەمتر لە 5 چرکەدا جێبەجێ دەبێت؛ N3 هیچ ڕاستەقینەی کڕیار نییە و نهێنییەکان هەرگیز ناچنە ناو . Won't have this time: پارەدان بە کارت، گەیاندن بۆ دەرەوەی سلێمانی و لیستی داواکارییە نوێیەکانی سۆران.
Step 2: two sprints on a boardهەنگاوی 2: دوو سپرینت لەسەر تەختەیەک
Split the work into two-week sprints (the development life cycle). Sprint 1 (25 October – 7 November 2026) delivers every Must and the Should, and ends with release 1.0.0 on 7 November. Sprint 2 (8 – 21 November) handles the first bug reports and BC-8, released as 1.1.0 on 20 November — the same dates as Bazaar Cart's story in versions and builds. Your own dates will be the days you work.
کارەکە بۆ دوو هەفتەیی دابەش بکە (خولی ژیانی گەشەپێدان). 1 (25ی تشرینی یەکەم – 7ی تشرینی دووەمی 2026) هەموو Mustـەکان و Shouldـەکە دەگەیەنێت و بە 1.0.0 لە 7ی تشرینی دووەم کۆتایی دێت. 2 (8 – 21ی تشرینی دووەم) یەکەم ڕاپۆرتەکانی و BC-8 دەگرێتە ئەستۆ کە وەک 1.1.0 لە 20ی تشرینی دووەم بڵاو دەکرێتەوە — هەمان ڕێکەوتەکانی چیرۆکی Bazaar Cart لە وەشانەکان و بیلدەکان. ڕێکەوتەکانی خۆت ئەو ڕۆژانە دەبن کە کاری تێدا دەکەیت.
Track the cards on a Kanban board with the columns To do → Doing → Testing → Done and a WIP limit of 2 in Doing. A card is done only when it meets the team's Definition of Done:
کارتەکان لەسەر تەختەیەکی بە ستوونەکانی To do ← Doing ← Testing ← Done و سنووری کاری لە دەستدای 2 لە Doing بەدواداچوونیان بۆ بکە. کارتێک تەنیا ئەو کاتە تەواوە کە لەگەڵ تیمەکە بگونجێت:
- all tests pass (
python3.13 -m unittest); - someone else reviewed the change (code review);
- README and CHANGELOG say what changed.
- هەموو تاقیکردنەوەکان سەردەکەون (
python3.13 -m unittest)؛ - کەسێکی تر گۆڕانکارییەکەی پێداچووەتەوە (پێداچوونەوەی کۆد)؛
- README و CHANGELOG دەڵێن چی گۆڕاوە.
Step 3: an empty repository with its first commitهەنگاوی 3: کۆگایەکی کۆدی بەتاڵ و یەکەم کۆمیتەکەی
Create the repository and three small files before any code (Git basics): a README that says how to run and test the app, a .gitignore that keeps caches, builds and secrets out, and a CHANGELOG with an empty Unreleased section.
پێش هەر کۆدێک و سێ فایلی بچووک دروست بکە (بنەماکانی Git): README کە دەڵێت چۆن جێبەجێ دەکرێت و تاقی دەکرێتەوە، .gitignore کە کاش، و نهێنییەکان لە دەرەوە دەهێڵێتەوە و CHANGELOG بە بەشێکی Unreleasedـی بەتاڵ.
git config --global user.name "Aram"
git config --global user.email "aram@example.com"
git init -b main bazaar-cart
cd bazaar-cart$ git config --global user.name "Aram" $ git config --global user.email "aram@example.com" $ git init -b main bazaar-cart Initialized empty Git repository in ~/se/bazaar-cart/.git/
# Python's cache and virtual environments
__pycache__/
*.pyc
.venv/
# Built files: made again from the source at any time
build/
dist/
# Secrets stay on your computer
.env# Changelog
All notable changes to Bazaar Cart are written here.
This project follows Semantic Versioning (https://semver.org).
## [Unreleased]git status --short
git add .
git commit -m "Start Bazaar Cart: README, .gitignore and CHANGELOG"$ git status --short ?? .gitignore ?? CHANGELOG.md ?? README.md $ git add . $ git commit -m "Start Bazaar Cart: README, .gitignore and CHANGELOG" [main (root-commit) 71349b1] Start Bazaar Cart: README, .gitignore and CHANGELOG 3 files changed, 29 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md
Step 4: design — layers, MVP and DI togetherهەنگاوی 4: دیزاین — چینەکان، MVP و DI پێکەوە
Before writing classes, draw how the parts fit. Three layers with one direction (layers and business logic): the data access layer knows SQL, the business logic layer knows Soran's rules R1–R5, and the user interface layer asks and shows. Inside the user interface, the screen follows MVP (MVC, MVP and MVVM): a passive console view and a presenter. Every object gets its parts by constructor injection, and one composition root, main() in app.py, builds them all (dependency injection).
پێش نووسینی ، بکێشە کە بەشەکان چۆن لەگەڵ یەکتر دەگونجێن. سێ بە یەک ئاراستە (چینەکان و لۆجیکی کاروبار): دەزانێت، یاساکانی سۆران R1–R5 دەزانێت و دەپرسێت و پیشان دەدات. لە ناو شاشەکە پەیڕەوی MVP دەکات (MVC، MVP و MVVM): کۆنسۆڵی ناچالاک و . هەر پارچەکانی بە وەردەگرێت و یەک ، main() لە app.py، هەموویان دروست دەکات (تێخستنی وابەستەیی).
Step 4: design — layers, MVP and DI togetherهەنگاوی 4: دیزاین — چینەکان، MVP و DI پێکەوە
Classes and filesکلاسەکان و فایلەکان
The class diagram extends the one from the requirements lesson (one class diagram) with the classes of the design, and the folder tree puts each class in its file. Each file has one job, so a change to one rule touches one file.
ئەوەی وانەی (یەک هێڵکاریی کلاسی UML) بە دیزاینەکە فراوان دەکات و دارە فۆڵدەرەکە هەر لە فایلی خۆیدا دادەنێت. هەر فایلێک یەک کاری هەیە، بۆیە گۆڕینی یەک یاسا دەست لە یەک فایل دەدات.
Step 5: the data access layerهەنگاوی 5: چینی دەستڕاگەیشتن بە دراوە
models.py and repository.py are exactly the files of the layers lesson: the dataclasses Product, OrderLine and Order, and a ProductRepository over an SQLite database in memory with Soran's five products. Copy them, then prove that the repository and the real database work together with an integration test:
models.py و repository.py ڕێک فایلەکانی وانەی چینەکانن: dataclassـەکانی Product، OrderLine و Order و ProductRepositoryـێک لەسەر لە ناو بیرگە بە پێنج کاڵاکەی سۆران. کۆپییان بکە، پاشان بە بیسەلمێنە کە و ڕاستەقینەکە پێکەوە کار دەکەن:
"""Integration tests: the repository with a real SQLite database (in memory)."""
import unittest
from bazaar_cart.repository import ProductRepository, open_demo_db
class ProductRepositoryTest(unittest.TestCase):
def setUp(self):
self.repo = ProductRepository(open_demo_db()) # a fresh database per test
def test_get_returns_the_product(self):
honey = self.repo.get("honey")
self.assertEqual((honey.name, honey.price_per_kg, honey.stock_g),
("Honey", 30000, 3000))
def test_unknown_code_returns_none(self):
self.assertIsNone(self.repo.get("saffron"))
def test_reduce_stock_is_saved(self):
self.repo.reduce_stock("sumac", 350)
self.assertEqual(self.repo.get("sumac").stock_g, 4650)
if __name__ == "__main__":
unittest.main()test_get_returns_the_product (tests.test_repository.ProductRepositoryTest.test_get_returns_the_product) ... ok test_reduce_stock_is_saved (tests.test_repository.ProductRepositoryTest.test_reduce_stock_is_saved) ... ok test_unknown_code_returns_none (tests.test_repository.ProductRepositoryTest.test_unknown_code_returns_none) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.009s OK
git add .
git commit -m "Add the product models and the SQLite repository"$ git add . $ git commit -m "Add the product models and the SQLite repository" [main f00c790] Add the product models and the SQLite repository 6 files changed, 90 insertions(+) create mode 100644 bazaar_cart/__init__.py create mode 100644 bazaar_cart/models.py create mode 100644 bazaar_cart/repository.py create mode 100644 tests/__init__.py create mode 100644 tests/test_repository.py
Step 6: the business layer, test firstهەنگاوی 6: چینی لۆجیکی کاروبار، تاقیکردنەوە لە پێشەوە
Write the tests for R1–R3 before the code (test first: red, green, refactor). Each acceptance example of BC-1 and BC-2 becomes one check, with the boundaries 99, 100, 10,000 and 10,050 g. A small fake repository stands in for the database, as in the layers lesson.
تاقیکردنەوەکانی R1–R3 پێش کۆدەکە بنووسە (تاقیکردنەوە لە پێشەوە: سوور، سەوز، ڕێکخستنەوە). هەر نموونەیەکی پەسەندکردنی BC-1 و BC-2 دەبێتە یەک پشکنین، لەگەڵ سنوورەکانی 99، 100، 10,000 و 10,050 گرام. ساختەی بچووک جێگەی دەگرێتەوە، وەک وانەی .
class OrderServiceTest(unittest.TestCase):
def setUp(self):
self.repo = InMemoryProductRepository([
Product("walnuts", "Walnuts", 18000, 20000),
Product("sumac", "Sumac", 8000, 5000),
Product("honey", "Honey", 30000, 3000)])
self.service = OrderService(self.repo)
def price(self, code, grams):
return self.service.price_order([(code, grams)]).lines[0].price
def test_r1_exact_price(self): # story BC-1, criterion 1
self.assertEqual(self.price("walnuts", 500), 9000)
def test_r1_rounds_up_to_250(self): # story BC-1, criterion 2
self.assertEqual(self.price("sumac", 350), 3000) # 2,800 -> 3,000
def test_r2_boundaries(self):
self.assertEqual(self.price("sumac", 100), 1000)
self.assertEqual(self.price("walnuts", 10_000), 180_000)
for grams in (99, 10_050):
with self.assertRaises(ValueError):
self.service.price_order([("sumac", grams)])
def test_r3_not_more_than_the_stock(self):
self.assertEqual(self.price("honey", 3000), 90_000)
with self.assertRaises(ValueError):
self.service.price_order([("honey", 3500)])Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 12-13
Story BC-1, criterion 1: an exact price.
چیرۆکی BC-1، پێوەری 1: نرخێکی ڕێک.
- Lineدێڕی 15-16
Story BC-1, criterion 2: 2,800 IQD is rounded up to 3,000.
چیرۆکی BC-1، پێوەری 2: 2,800 دینار بەرز دەکرێتەوە بۆ 3,000.
E
======================================================================
ERROR: test_services (unittest.loader._FailedTest.test_services)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_services
Traceback (most recent call last):
File "/opt/homebrew/Cellar/python@3.13/3.13.17/Frameworks/Python.framework/Versions/3.13/lib/python3.13/unittest/loader.py", line 141, in loadTestsFromName
module = __import__(module_name)
File "~/se/bazaar-cart/tests/test_services.py", line 5, in <module>
from bazaar_cart.services import OrderService
ModuleNotFoundError: No module named 'bazaar_cart.services'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)Now write services.py as in the layers lesson: round_up_250() with math.ceil, line_price(), and OrderService.price_order() checking R2 and R3. Delivery stays 0 until R4 arrives on its own branch in step 9. Run the tests again:
ئێستا services.py وەک وانەی چینەکان بنووسە: round_up_250() بە math.ceil، line_price() و OrderService.price_order() کە R2 و R3 دەپشکنێت. کرێی گەیاندن 0 دەمێنێتەوە تا R4 لە هەنگاوی 9دا لەسەر خۆی دێت. تاقیکردنەوەکان دیسان جێبەجێ بکە:
test_r1_exact_price (tests.test_services.OrderServiceTest.test_r1_exact_price) ... ok test_r1_rounds_up_to_250 (tests.test_services.OrderServiceTest.test_r1_rounds_up_to_250) ... ok test_r2_boundaries (tests.test_services.OrderServiceTest.test_r2_boundaries) ... ok test_r3_not_more_than_the_stock (tests.test_services.OrderServiceTest.test_r3_not_more_than_the_stock) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK


git add .
git commit -m "Add line prices and checks for R1, R2 and R3"$ git add . $ git commit -m "Add line prices and checks for R1, R2 and R3" [main 08d547b] Add line prices and checks for R1, R2 and R3 3 files changed, 91 insertions(+) create mode 100644 bazaar_cart/services.py create mode 100644 tests/test_services.py
Step 7: the presenter and the console viewهەنگاوی 7: پێشکەشکەر و دیمەنی کۆنسۆڵ
The screen follows the MVP code of the patterns lesson, with one addition for BC-3 and BC-4: on_confirm(phone) places the order. The presenter never calls input() or print(); the view never knows a rule.
شاشەکە پەیڕەوی کۆدی MVPـی وانەی شێوازەکان دەکات، بە یەک زیادکردن بۆ BC-3 و BC-4: on_confirm(phone) داواکارییەکە تۆمار دەکات. هەرگیز input() یان print() بانگ ناکات؛ هەرگیز یاسایەک نازانێت.
"""Presenter (MVP): decides what the cart screen shows. No input(), no print()."""
from typing import Protocol
class CartView(Protocol): # what every cart view must offer
def show_lines(self, lines: list[str]) -> None: ...
def show_total(self, text: str) -> None: ...
def show_error(self, message: str) -> None: ...
def show_message(self, text: str) -> None: ...
class CartPresenter:
def __init__(self, service, view: CartView) -> None:
self.service = service
self.view = view
self.items = []
def on_add(self, code, grams_text): # the view calls this for every "Add"
if not grams_text.isdigit():
self.view.show_error("Type the grams as a whole number.")
return
try:
order = self.service.price_order(self.items + [(code, int(grams_text))])
except ValueError as error: # a business rule said no
self.view.show_error(str(error))
return
self.items.append((code, int(grams_text)))
self.view.show_lines([f"{line.name} {line.grams} g: {line.price:,} IQD"
for line in order.lines])
self.view.show_total(f"Total {order.total:,} IQD (delivery {order.delivery:,})")
def on_confirm(self, phone): # the view calls this at the end
if not self.items:
self.view.show_error("The cart is empty.")
return
if not phone:
self.view.show_error("Type a phone number for the SMS.")
return
try:
total = self.service.place_order(self.items, phone)
except ValueError as error:
self.view.show_error(str(error))
return
self.items = []
self.view.show_message(f"Order placed: {total:,} IQD. Thank you!")"""Console view (MVP): it reads keys and prints what the presenter tells it."""
class ConsoleCartView:
def show_lines(self, lines):
for line in lines:
print(" " + line)
def show_total(self, text):
print(" " + text)
def show_error(self, message):
print(" ! " + message)
def show_message(self, text):
print(text)
def run(self, presenter):
while command := input("Add (code grams), empty to stop: ").strip():
code, _, grams = command.partition(" ")
presenter.on_add(code, grams)
presenter.on_confirm(input("Phone for the SMS: ").strip())Step 7: the presenter and the console viewهەنگاوی 7: پێشکەشکەر و دیمەنی کۆنسۆڵ
Wiring it in app.pyبەستنەوەی لە app.py
OrderService now takes a notifier, exactly as in the dependency injection lesson, and app.py is the composition root. With no real customers, the project uses ConsoleNotifier; __main__.py lets you start the app with python3.13 -m bazaar_cart.
ئێستا OrderService ئاگادارکەرەوەیەک وەردەگرێت، ڕێک وەک وانەی تێخستنی وابەستەیی و app.py . چونکە کڕیاری ڕاستەقینە نییە، پڕۆژەکە ConsoleNotifier بەکار دەهێنێت؛ __main__.py ڕێگە دەدات بە python3.13 -m bazaar_cart دەست پێبکەیت.
"""Composition root: the one place where Bazaar Cart's objects are made and connected."""
from .cli import ConsoleCartView
from .notifiers import ConsoleNotifier
from .presenter import CartPresenter
from .repository import ProductRepository, open_demo_db
from .services import OrderService
def main():
repo = ProductRepository(open_demo_db()) # data access layer
service = OrderService(repo, ConsoleNotifier()) # business layer + its notifier
view = ConsoleCartView() # presentation: the view ...
view.run(CartPresenter(service, view)) # ... and its presenter"""Lets you start the app with: python3.13 -m bazaar_cart"""
from .app import main
main()$ python3.13 -m bazaar_cart Add (code grams), empty to stop: walnuts 500 Walnuts 500 g: 9,000 IQD Total 9,000 IQD (delivery 0) Add (code grams), empty to stop: sumac half ! Type the grams as a whole number. Add (code grams), empty to stop: sumac 350 Walnuts 500 g: 9,000 IQD Sumac 350 g: 3,000 IQD Total 12,000 IQD (delivery 0) Add (code grams), empty to stop: Phone for the SMS: 0770 000 0001 [SMS to 0770 000 0001] Bazaar Cart: your order of 12,000 IQD is on its way. Order placed: 12,000 IQD. Thank you!
Step 8: every test green, every layer testedهەنگاوی 8: هەموو تاقیکردنەوەکان سەوز، هەموو چینێک تاقیکراوە
Add the presenter tests (a fake view, as in testing a presenter) and two notifier tests with a FakeNotifier. Then run the whole suite. python3.13 -m unittest finds every test*.py file by itself:
تاقیکردنەوەکانی زیاد بکە ( ساختە، وەک تاقیکردنەوەی پێشکەشکەرێک) و دوو تاقیکردنەوەی ئاگادارکەرەوە بە FakeNotifier. پاشان هەموو جێبەجێ بکە. python3.13 -m unittest خۆی هەموو فایلێکی test*.py دەدۆزێتەوە:
python3.13 -m unittest -v$ python3.13 -m unittest -v test_adding_walnuts_shows_the_line_and_the_total (tests.test_presenter.CartPresenterTest.test_adding_walnuts_shows_the_line_and_the_total) ... ok test_confirm_places_the_order_and_sends_the_sms (tests.test_presenter.CartPresenterTest.test_confirm_places_the_order_and_sends_the_sms) ... ok test_confirm_with_an_empty_cart (tests.test_presenter.CartPresenterTest.test_confirm_with_an_empty_cart) ... ok test_letters_for_grams_show_an_error (tests.test_presenter.CartPresenterTest.test_letters_for_grams_show_an_error) ... ok test_get_returns_the_product (tests.test_repository.ProductRepositoryTest.test_get_returns_the_product) ... ok test_reduce_stock_is_saved (tests.test_repository.ProductRepositoryTest.test_reduce_stock_is_saved) ... ok test_unknown_code_returns_none (tests.test_repository.ProductRepositoryTest.test_unknown_code_returns_none) ... ok test_no_sms_when_a_rule_says_no (tests.test_services.OrderServiceTest.test_no_sms_when_a_rule_says_no) ... ok test_place_order_takes_stock_and_sends_one_sms (tests.test_services.OrderServiceTest.test_place_order_takes_stock_and_sends_one_sms) ... ok test_r1_exact_price (tests.test_services.OrderServiceTest.test_r1_exact_price) ... ok test_r1_rounds_up_to_250 (tests.test_services.OrderServiceTest.test_r1_rounds_up_to_250) ... ok test_r2_boundaries (tests.test_services.OrderServiceTest.test_r2_boundaries) ... ok test_r3_not_more_than_the_stock (tests.test_services.OrderServiceTest.test_r3_not_more_than_the_stock) ... ok ---------------------------------------------------------------------- Ran 13 tests in 0.003s OK
git add .
git commit -m "Add the console cart (MVP), wired in app.py"
git log --oneline$ git add . $ git commit -m "Add the console cart (MVP), wired in app.py" [main 435ce4f] Add the console cart (MVP), wired in app.py 9 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 bazaar_cart/__main__.py create mode 100644 bazaar_cart/app.py create mode 100644 bazaar_cart/cli.py create mode 100644 bazaar_cart/notifiers.py create mode 100644 bazaar_cart/presenter.py create mode 100644 tests/test_presenter.py $ git log --oneline 435ce4f Add the console cart (MVP), wired in app.py 08d547b Add line prices and checks for R1, R2 and R3 f00c790 Add the product models and the SQLite repository 71349b1 Start Bazaar Cart: README, .gitignore and CHANGELOG
Step 9: the delivery fee on its own branchهەنگاوی 9: کرێی گەیاندن لەسەر لقی خۆی
BC-7 (R4) is built on a feature branch, so main keeps working while it grows (branches and merging). Karwan writes the two tests from BC-7's acceptance criteria first — 49,750 IQD pays 3,000, exactly 50,000 travels free — and updates the presenter tests, whose totals now include delivery:
BC-7 (R4) لەسەر دروست دەکرێت، بۆیە main لە کاتی گەشەکردنیدا هەر کار دەکات (لقەکان و بەیەککردن). کاروان سەرەتا دوو تاقیکردنەوەکە لە BC-7 دەنووسێت — 49,750 دینار 3,000 دەدات، ڕێک 50,000 بێبەرانبەر دەگات — و تاقیکردنەوەکانی نوێ دەکاتەوە کە ئێستا کۆکانیان کرێی گەیاندنیش دەگرێتەوە:
git switch -c feature/delivery-feeSwitched to a new branch 'feature/delivery-fee'
def test_r4_delivery_below_50000(self): # 49,750 IQD of goods
order = self.service.price_order([("honey", 1000), ("walnuts", 1000),
("sumac", 200)])
self.assertEqual((order.goods_total, order.delivery), (49_750, 3_000))
def test_r4_free_delivery_from_50000(self): # exactly 50,000 IQD
order = self.service.price_order([("honey", 1000), ("walnuts", 1000),
("sumac", 250)])
self.assertEqual((order.goods_total, order.delivery), (50_000, 0))FF...........F.
======================================================================
FAIL: test_r4_delivery_below_50000 (tests.test_services.OrderServiceTest.test_r4_delivery_below_50000)
----------------------------------------------------------------------
Traceback (most recent call last):
File "~/se/bazaar-cart/tests/test_services.py", line 64, in test_r4_delivery_below_50000
self.assertEqual((order.goods_total, order.delivery), (49_750, 3_000))
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Tuples differ: (49750, 0) != (49750, 3000)
First differing element 1:
0
3000
- (49750, 0)
+ (49750, 3000)
? +++
[… 2 more failure(s) cut here …]
----------------------------------------------------------------------
Ran 15 tests in 0.013s
FAILED (failures=3)def delivery_fee(goods_total):
"""R4: 3,000 IQD, free from 50,000 IQD of goods."""
return 0 if goods_total >= FREE_DELIVERY_FROM else DELIVERY_FEEpython3.13 -m unittest
git commit -am "Add the delivery fee (R4)"$ python3.13 -m unittest ............... ---------------------------------------------------------------------- Ran 15 tests in 0.006s OK $ git commit -am "Add the delivery fee (R4)" [feature/delivery-fee 10618ec] Add the delivery fee (R4) 4 files changed, 22 insertions(+), 4 deletions(-)
Meanwhile Shilan adds the version number to the start screen on main and, following the Definition of Done, writes it into the CHANGELOG — in the same place where Karwan wrote his line.
لەو کاتەدا شیلان ژمارەی لە سەر main دەخاتە دێڕی یەکەمی و بەپێی لە CHANGELOGـدا دەینووسێت — لە هەمان شوێن کە کاروان دێڕەکەی خۆی نووسیبوو.
git switch main
git commit -am "Show the version number at start-up"$ git switch main Switched to branch 'main' $ git commit -am "Show the version number at start-up" [main 6f01b28] Show the version number at start-up 2 files changed, 3 insertions(+)
Step 9: the delivery fee on its own branchهەنگاوی 9: کرێی گەیاندن لەسەر لقی خۆی
Merge, resolve the conflict, delete the branchبەیەککردن، چارەسەرکردنی ناکۆکی، سڕینەوەی لقەکە
git merge feature/delivery-fee
git status$ git merge feature/delivery-fee Auto-merging CHANGELOG.md CONFLICT (content): Merge conflict in CHANGELOG.md Automatic merge failed; fix conflicts and then commit the result. $ git status On branch main You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Changes to be committed: modified: bazaar_cart/services.py modified: tests/test_presenter.py modified: tests/test_services.py Unmerged paths: (use "git add <file>..." to mark resolution) both modified: CHANGELOG.md
## [Unreleased]
### Added
- Five products with their stock in an SQLite database.
- Line prices rounded up to 250 IQD (R1), 100 g to 10,000 g per line (R2)
and never more than the stock (R3).
- Console cart (MVP) with an SMS confirmation.
<<<<<<< HEAD
- Version number on the start screen.
=======
- Delivery fee: 3,000 IQD inside Sulaimani, free from 50,000 IQD (R4).
>>>>>>> feature/delivery-feeBoth lines belong in the changelog, so keep both and delete the three marker lines. Run the tests before you finish the merge — leftover markers in a .py file would be a SyntaxError (leftover markers):
هەردوو دێڕەکە شوێنیان لە ، بۆیە هەردووکیان بهێڵەوە و سێ دێڕی نیشانەکان بسڕەوە. پێش تەواوکردنی تاقیکردنەوەکان جێبەجێ بکە — نیشانەی ماوە لە فایلێکی .pyـدا دەبێتە SyntaxError (نیشانە ماوەکان):
python3.13 -m unittest
git add CHANGELOG.md
git commit --no-edit
git branch -d feature/delivery-fee
git log --oneline --graph$ python3.13 -m unittest ............... ---------------------------------------------------------------------- Ran 15 tests in 0.003s OK $ git add CHANGELOG.md $ git commit --no-edit [main 6c44cf3] Merge branch 'feature/delivery-fee' $ git branch -d feature/delivery-fee Deleted branch feature/delivery-fee (was 10618ec). $ git log --oneline --graph * 6c44cf3 Merge branch 'feature/delivery-fee' |\ | * 10618ec Add the delivery fee (R4) * | 6f01b28 Show the version number at start-up |/ * 435ce4f Add the console cart (MVP), wired in app.py * 08d547b Add line prices and checks for R1, R2 and R3 * f00c790 Add the product models and the SQLite repository * 71349b1 Start Bazaar Cart: README, .gitignore and CHANGELOG
Step 10: release 1.0.0هەنگاوی 10: وەشانی بڵاوکراوە 1.0.0
Every Must is done, so Bazaar Cart becomes 1.0.0: the first stable version that people may depend on (what 1.0.0 means). Four small changes make a release: the version number in bazaar_cart/__init__.py (__version__ = "1.0.0"), the Unreleased lines moved under ## [1.0.0] - 2026-11-07 in the CHANGELOG, a build script, and an annotated tag:
هەموو Mustـەکان تەواو بوون، بۆیە Bazaar Cart دەبێتە 1.0.0: یەکەم جێگیر کە خەڵک دەتوانن پشتی پێ ببەستن (1.0.0 واتای چییە). چوار گۆڕانکاریی بچووک دروست دەکەن: ژمارەی لە bazaar_cart/__init__.py (__version__ = "1.0.0")، گواستنەوەی دێڕەکانی Unreleased بۆ ژێر ## [1.0.0] - 2026-11-07 لە CHANGELOG، سکریپتێکی بیلد و تاگێکی annotated:
"""Build dist/bazaar_cart-<version>.pyz from the bazaar_cart package (zipapp, as in python -m zipapp)."""
import pathlib
import zipapp
from bazaar_cart import __version__
target = pathlib.Path("dist") / f"bazaar_cart-{__version__}.pyz"
target.parent.mkdir(exist_ok=True)
zipapp.create_archive(
".", target, main="bazaar_cart.app:main", interpreter="/usr/bin/env python3",
filter=lambda path: path.parts[0] == "bazaar_cart" and "__pycache__" not in path.parts)
print(f"Built {target} ({target.stat().st_size:,} bytes)")Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 9-11
The same zipapp as
python -m zipappin versions and builds; the filter keeps only the package.هەمان zipappـی
python -m zipappلە وەشانەکان و بیلدەکان؛ فلتەرەکە تەنیا پاکێجەکە دەهێڵێتەوە.
git add .
git commit -m "Release 1.0.0"
git tag -a v1.0.0 -m "Bazaar Cart 1.0.0: first release"
git show v1.0.0 --no-patch$ git add .
$ git commit -m "Release 1.0.0"
[main f8e00f8] Release 1.0.0
3 files changed, 15 insertions(+), 1 deletion(-)
create mode 100644 build.py
$ git tag -a v1.0.0 -m "Bazaar Cart 1.0.0: first release"
$ git show v1.0.0 --no-patch
tag v1.0.0
Tagger: Aram <aram@example.com>
Date: Sat Nov 7 16:01:00 2026 +0300
Bazaar Cart 1.0.0: first release
commit f8e00f86127b21c9c2bcd44baf7bd4262056c59e
Author: Aram <aram@example.com>
Date: Sat Nov 7 16:00:00 2026 +0300
Release 1.0.0$ python3.13 build.py Built dist/bazaar_cart-1.0.0.pyz (8,984 bytes) $ python3.13 dist/bazaar_cart-1.0.0.pyz Bazaar Cart 1.0.0 - Soran's shop, Sulaimani bazaar Add (code grams), empty to stop: walnuts 500 Walnuts 500 g: 9,000 IQD Total 12,000 IQD (delivery 3,000) Add (code grams), empty to stop: sumac 350 Walnuts 500 g: 9,000 IQD Sumac 350 g: 3,000 IQD Total 15,000 IQD (delivery 3,000) Add (code grams), empty to stop: Phone for the SMS: 0770 000 0001 [SMS to 0770 000 0001] Bazaar Cart: your order of 15,000 IQD is on its way. Order placed: 15,000 IQD. Thank you!
Step 11: a bug report, a regression test and 1.0.1هەنگاوی 11: ڕاپۆرتی باگ، تاقیکردنەوەی پاشەکشە و 1.0.1
Two days after the release, Dilan opens issue #7 (a good bug report). Notice which bug did not come back: Bazaar Cart's real 1.0.0 shipped a rounding bug because that team had no tests yet; your test_r1_rounds_up_to_250 has guarded R1 since step 6. Tests only check what someone thought of, though, and nobody thought of the same product on two lines:
دوو ڕۆژ دوای ، دیلان #7 دەکاتەوە (ڕاپۆرتێکی باشی باگ). سەرنج بدە کام نەگەڕایەوە: 1.0.0ـی ڕاستەقینەی Bazaar Cart خڕکردنەوەی تێدا بوو چونکە ئەو تیمە هێشتا تاقیکردنەوەی نەبوو؛ test_r1_rounds_up_to_250ـی تۆ لە هەنگاوی 6ـەوە پاسەوانی R1 دەکات. بەڵام تاقیکردنەوەکان تەنیا ئەوە دەپشکنن کە کەسێک بیری لێ کردبێتەوە و کەس بیری لە هەمان کاڵا لەسەر دوو دێڕ نەکردبووەوە:
Title: Two lines of honey take more than the stock #7
Version: 1.0.0 (bazaar_cart-1.0.0.pyz)
Steps to reproduce:
1. Start the app; honey has 3,000 g in stock.
2. Add "honey 2000", then "honey 2000" again.
3. Confirm with any phone number.
Expected: "Honey: only 3,000 g in stock".
Actual: the order of 120,000 IQD is placed; the stock becomes -1,000 g.
Labels: bug Assignee: Shilan Milestone: 1.0.1$ python3.13 dist/bazaar_cart-1.0.0.pyz Bazaar Cart 1.0.0 - Soran's shop, Sulaimani bazaar Add (code grams), empty to stop: honey 2000 Honey 2000 g: 60,000 IQD Total 60,000 IQD (delivery 0) Add (code grams), empty to stop: honey 2000 Honey 2000 g: 60,000 IQD Honey 2000 g: 60,000 IQD Total 120,000 IQD (delivery 0) Add (code grams), empty to stop: Phone for the SMS: 0770 000 0002 [SMS to 0770 000 0002] Bazaar Cart: your order of 120,000 IQD is on its way. Order placed: 120,000 IQD. Thank you!
A three-line script confirms the stock: Honey left in stock: -1000 g. The cause is in price_order(): R3 compares each line with the stock on its own, so two lines of 2,000 g both pass. BC-2's acceptance criterion had only one line per product — a gap in the requirements, not just in the code.
سکریپتێکی سێ دێڕی کۆگاکە دەسەلمێنێت: Honey left in stock: -1000 g. هۆکارەکە لە price_order()ـدایە: R3 هەر دێڕێک بە تەنیا لەگەڵ کۆگا بەراورد دەکات، بۆیە دوو دێڕی 2,000 گرامی هەردووکیان تێدەپەڕن. BC-2 تەنیا یەک دێڕی بۆ هەر کاڵایەک هەبوو — کەلێنێک لە ، نەک تەنیا لە کۆدەکەدا.
Step 11: a bug report, a regression test and 1.0.1هەنگاوی 11: ڕاپۆرتی باگ، تاقیکردنەوەی پاشەکشە و 1.0.1
Red, fix, green, release 1.0.1سوور، چاکسازی، سەوز، وەشانی بڵاوکراوە 1.0.1
Shilan works on a fix/ branch. First the regression test — it must fail on 1.0.0 (every bug gets a test) — then the fix: count the grams of each product over all its lines.
شیلان لەسەر fix/ کار دەکات. سەرەتا — دەبێت لەسەر 1.0.0 شکست بهێنێت (هەموو باگێک تاقیکردنەوەیەک وەردەگرێت) — پاشان چاکسازییەکە: گرامی هەر کاڵایەک لە هەموو دێڕەکانیدا بژمێرە.
git switch -c fix/stock-two-linesSwitched to a new branch 'fix/stock-two-lines'
def test_r3_two_lines_of_one_product_count_together(self): # issue #7
with self.assertRaises(ValueError):
self.service.price_order([("honey", 2000), ("honey", 2000)])......F..
======================================================================
FAIL: test_r3_two_lines_of_one_product_count_together (tests.test_services.OrderServiceTest.test_r3_two_lines_of_one_product_count_together)
----------------------------------------------------------------------
Traceback (most recent call last):
File "~/se/bazaar-cart/tests/test_services.py", line 62, in test_r3_two_lines_of_one_product_count_together
with self.assertRaises(ValueError):
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
AssertionError: ValueError not raised
----------------------------------------------------------------------
Ran 9 tests in 0.002s
FAILED (failures=1) def price_order(self, items):
"""Check every line (R2, R3) and price the order (R1, R4). Saves nothing."""
lines, wanted = [], {} # wanted: grams per product in this order
for code, grams in items:
product = self.repo.get(code)
if product is None:
raise ValueError(f"unknown product: {code}")
if not MIN_GRAMS <= grams <= MAX_GRAMS:
raise ValueError(f"{product.name}: {grams} g is outside 100-10,000 g")
wanted[code] = wanted.get(code, 0) + grams
if wanted[code] > product.stock_g: # all lines of it together (#7)
raise ValueError(f"{product.name}: only {product.stock_g:,} g in stock")
lines.append(OrderLine(product.name, grams,
line_price(product.price_per_kg, grams)))python3.13 -m unittest
git commit -am "Count all lines of a product in the stock check (fixes #7)"
git switch main
git merge fix/stock-two-lines
git branch -d fix/stock-two-lines
git commit -am "Release 1.0.1"
git tag -a v1.0.1 -m "Bazaar Cart 1.0.1: stock check fix"$ python3.13 -m unittest ................ ---------------------------------------------------------------------- Ran 16 tests in 0.003s OK $ git commit -am "Count all lines of a product in the stock check (fixes #7)" [fix/stock-two-lines 37237e7] Count all lines of a product in the stock check (fixes #7) 3 files changed, 10 insertions(+), 2 deletions(-) $ git switch main Switched to branch 'main' $ git merge fix/stock-two-lines Updating f8e00f8..37237e7 Fast-forward CHANGELOG.md | 3 +++ bazaar_cart/services.py | 5 +++-- tests/test_services.py | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) $ git branch -d fix/stock-two-lines Deleted branch fix/stock-two-lines (was 37237e7). $ git commit -am "Release 1.0.1" [main 67c5571] Release 1.0.1 2 files changed, 3 insertions(+), 1 deletion(-) $ git tag -a v1.0.1 -m "Bazaar Cart 1.0.1: stock check fix"
The merge was a fast-forward: main had not moved, so Git only moved the name (fast-forward). The CHANGELOG got a ### Fixed line, and the commit message says fixes #7, which also closes the issue when the commit reaches GitHub's default branch.
بوو: main نەجووڵابوو، بۆیە Git تەنیا ناوەکەی جووڵاند (بەیەککردنی بەپێشخستن). CHANGELOG دێڕێکی ### Fixedـی وەرگرت و پەیامی دەڵێت fixes #7، کە کاتێک دەگاتە بنەڕەتیی GitHub دادەخات.
Step 11: a bug report, a regression test and 1.0.1هەنگاوی 11: ڕاپۆرتی باگ، تاقیکردنەوەی پاشەکشە و 1.0.1
Step 12: the discount and 1.1.0هەنگاوی 12: داشکاندن و 1.1.0
BC-8 adds R5: 5 % off goods totals from 100,000 IQD, rounded up to 250 — the same discount() as in automated testing. Test first on feature/discount: the tests import discount, so the first run is red with an ImportError. Then the code, and Order gets a new field discount with the default 0, so code written for 1.0 keeps working: a MINOR version.
BC-8 R5 زیاد دەکات: 5 لە سەد داشکاندن بۆ کۆی کاڵای 100,000 دینار و زیاتر، بەرزکراوە بۆ 250 — هەمان discount()ـی تاقیکردنەوەی خۆکار. لەسەر feature/discount تاقیکردنەوە لە پێشەوە: تاقیکردنەوەکان discount هاوردە دەکەن، بۆیە یەکەم جێبەجێکردن بە ImportError سوورە. پاشان کۆدەکە و Order خانەیەکی نوێی discount بە بنەڕەتی 0 وەردەگرێت، بۆیە ئەو کۆدەی بۆ 1.0 نووسرابوو هەر کار دەکات: .
def discount(goods_total):
"""R5: 5 % off goods totals from 100,000 IQD, rounded up to 250 IQD."""
if goods_total < DISCOUNT_FROM:
return 0
return round_up_250(goods_total * DISCOUNT_PERCENT / 100)python3.13 -m unittest -v$ python3.13 -m unittest -v test_adding_walnuts_shows_the_line_and_the_total (tests.test_presenter.CartPresenterTest.test_adding_walnuts_shows_the_line_and_the_total) ... ok test_confirm_places_the_order_and_sends_the_sms (tests.test_presenter.CartPresenterTest.test_confirm_places_the_order_and_sends_the_sms) ... ok test_confirm_with_an_empty_cart (tests.test_presenter.CartPresenterTest.test_confirm_with_an_empty_cart) ... ok test_letters_for_grams_show_an_error (tests.test_presenter.CartPresenterTest.test_letters_for_grams_show_an_error) ... ok test_get_returns_the_product (tests.test_repository.ProductRepositoryTest.test_get_returns_the_product) ... ok test_reduce_stock_is_saved (tests.test_repository.ProductRepositoryTest.test_reduce_stock_is_saved) ... ok test_unknown_code_returns_none (tests.test_repository.ProductRepositoryTest.test_unknown_code_returns_none) ... ok test_no_sms_when_a_rule_says_no (tests.test_services.OrderServiceTest.test_no_sms_when_a_rule_says_no) ... ok test_place_order_takes_stock_and_sends_one_sms (tests.test_services.OrderServiceTest.test_place_order_takes_stock_and_sends_one_sms) ... ok test_r1_exact_price (tests.test_services.OrderServiceTest.test_r1_exact_price) ... ok test_r1_rounds_up_to_250 (tests.test_services.OrderServiceTest.test_r1_rounds_up_to_250) ... ok test_r2_boundaries (tests.test_services.OrderServiceTest.test_r2_boundaries) ... ok test_r3_not_more_than_the_stock (tests.test_services.OrderServiceTest.test_r3_not_more_than_the_stock) ... ok test_r3_two_lines_of_one_product_count_together (tests.test_services.OrderServiceTest.test_r3_two_lines_of_one_product_count_together) ... ok test_r4_delivery_below_50000 (tests.test_services.OrderServiceTest.test_r4_delivery_below_50000) ... ok test_r4_free_delivery_from_50000 (tests.test_services.OrderServiceTest.test_r4_free_delivery_from_50000) ... ok test_r5_discount_boundaries (tests.test_services.OrderServiceTest.test_r5_discount_boundaries) ... ok test_r5_discount_in_the_order (tests.test_services.OrderServiceTest.test_r5_discount_in_the_order) ... ok ---------------------------------------------------------------------- Ran 18 tests in 0.004s OK
git commit -am "Add the 5 % discount from 100,000 IQD (R5)"
git switch main
git merge feature/discount
git branch -d feature/discount
git commit -am "Release 1.1.0"
git tag -a v1.1.0 -m "Bazaar Cart 1.1.0: discount from 100,000 IQD"
python3.13 build.py$ git commit -am "Add the 5 % discount from 100,000 IQD (R5)" [feature/discount a60de87] Add the 5 % discount from 100,000 IQD (R5) 5 files changed, 31 insertions(+), 5 deletions(-) $ git switch main Switched to branch 'main' $ git merge feature/discount Updating 67c5571..a60de87 Fast-forward CHANGELOG.md | 3 +++ bazaar_cart/models.py | 3 ++- bazaar_cart/presenter.py | 4 +++- bazaar_cart/services.py | 13 +++++++++++-- tests/test_services.py | 13 ++++++++++++- 5 files changed, 31 insertions(+), 5 deletions(-) $ git branch -d feature/discount Deleted branch feature/discount (was a60de87). $ git commit -am "Release 1.1.0" [main 2ce197d] Release 1.1.0 2 files changed, 3 insertions(+), 1 deletion(-) $ git tag -a v1.1.0 -m "Bazaar Cart 1.1.0: discount from 100,000 IQD" $ python3.13 build.py Built dist/bazaar_cart-1.1.0.pyz (9,707 bytes)
$ python3.13 dist/bazaar_cart-1.1.0.pyz Bazaar Cart 1.1.0 - Soran's shop, Sulaimani bazaar Add (code grams), empty to stop: honey 2000 Honey 2000 g: 60,000 IQD Total 60,000 IQD (delivery 0) Add (code grams), empty to stop: walnuts 2000 Honey 2000 g: 60,000 IQD Walnuts 2000 g: 36,000 IQD Total 96,000 IQD (delivery 0) Add (code grams), empty to stop: sumac 500 Honey 2000 g: 60,000 IQD Walnuts 2000 g: 36,000 IQD Sumac 500 g: 4,000 IQD Total 95,000 IQD (delivery 0, discount 5,000) Add (code grams), empty to stop: Phone for the SMS: 0770 000 0003 [SMS to 0770 000 0003] Bazaar Cart: your order of 95,000 IQD is on its way. Order placed: 95,000 IQD. Thank you!
git log --oneline --graph --decorate
git tag -n$ git log --oneline --graph --decorate * 2ce197d (HEAD -> main, tag: v1.1.0) Release 1.1.0 * a60de87 Add the 5 % discount from 100,000 IQD (R5) * 67c5571 (tag: v1.0.1) Release 1.0.1 * 37237e7 Count all lines of a product in the stock check (fixes #7) * f8e00f8 (tag: v1.0.0) Release 1.0.0 * 6c44cf3 Merge branch 'feature/delivery-fee' |\ | * 10618ec Add the delivery fee (R4) * | 6f01b28 Show the version number at start-up |/ * 435ce4f Add the console cart (MVP), wired in app.py * 08d547b Add line prices and checks for R1, R2 and R3 * f00c790 Add the product models and the SQLite repository * 71349b1 Start Bazaar Cart: README, .gitignore and CHANGELOG $ git tag -n v1.0.0 Bazaar Cart 1.0.0: first release v1.0.1 Bazaar Cart 1.0.1: stock check fix v1.1.0 Bazaar Cart 1.1.0: discount from 100,000 IQD
The marking checklist and the retrospectiveلیستی نمرەدان و ئاوڕدانەوە
A teacher (or the team itself) can mark the project with this checklist. Each line can be checked in the repository, not just in a report.
مامۆستا (یان خودی تیمەکە) دەتوانێت بەم لیستە نمرە بداتە پڕۆژەکە. هەر دێڕێک لە دەپشکنرێت، نەک تەنیا لە ڕاپۆرتێکدا.
| Areaبەش | What to checkچی بپشکنرێت | Pointsخاڵ |
|---|---|---|
| Requirements and plan و پلان | stories with acceptance examples, 3 non-functional requirements, MoSCoW, board, Definition of Doneچیرۆک بە نموونەی پەسەندکردن، 3 ، MoSCoW، تەختە، | 15 |
| Git historyمێژووی Git | small commits with clear messages; a feature branch; a resolved conflict; annotated tags بچووک بە پەیامی ڕوون؛ ؛ ناکۆکییەکی چارەسەرکراو؛ تاگی annotated | 15 |
| Testsتاقیکردنەوەکان | every rule R1–R5 tested with boundaries; an integration test; a regression test; all greenهەموو یاسایەکی R1–R5 بە سنوورەکانەوە تاقیکراوە؛ ؛ ؛ هەمووی سەوز | 25 |
| Designدیزاین | three layers, no SQL or print in the business layer; MVP; constructor injection; one composition rootسێ ، هیچ یان print لە نییە؛ MVP؛ ؛ یەک | 20 |
| Release | correct version numbers, CHANGELOG, a .pyz that runsژمارە ڕاستەکانی ، CHANGELOG، .pyzـێک کە کار دەکات | 15 |
| README and teamworkREADME و کاری تیمی | how to run and test; reviews; an honest retrospectiveچۆنیەتیی جێبەجێکردن و تاقیکردنەوە؛ پێداچوونەوەکان؛ ئاوڕدانەوەیەکی ڕاستگۆیانە | 10 |
Retrospective questions
پرسیارەکانی ئاوڕدانەوە
- What went well and should stay?
- What slowed us down (a conflict, a late review, an unclear story)?
- Which bug got through, and which test or acceptance criterion would have caught it? (#7: one more example for BC-2.)
- What is our one change for the next sprint?
- چی باش بوو و دەبێت بمێنێتەوە؟
- چی خاوی کردینەوە (ناکۆکییەک، پێداچوونەوەیەکی دواکەوتوو، چیرۆکێکی ناڕوون)؟
- کام تێپەڕی و کام تاقیکردنەوە یان دەیگرت؟ (#7: نموونەیەکی تر بۆ BC-2.)
- تاکە گۆڕانکاریمان بۆ داهاتوو چییە؟
Exercisesڕاهێنانەکان
Bazaar Cart is at 1.1.0. Give the next version for each change on its own: (a) a typo in the start message; (b) R6, delivery to Erbil for 6,000 IQD, while every old call still works; (c) place_order() returns an Order receipt instead of an int, so the shop's website must change.
Bazaar Cart لە 1.1.0ـدایە. بۆ هەر گۆڕانکارییەک بە تەنیا داهاتوو بڵێ: (a) هەڵەیەکی نووسین لە پەیامی دەستپێکدا؛ (b) R6، گەیاندن بۆ هەولێر بە 6,000 دینار، لە کاتێکدا هەموو بانگکردنە کۆنەکان هەر کار دەکەن؛ (c) place_order() لەبری پسوولەیەکی Order دەگەڕێنێتەوە، بۆیە ماڵپەڕی دوکانەکە دەبێت بگۆڕێت.
Show answerوەڵامەکە پیشان بدە
(a) 1.1.1 — a fix (PATCH). (b) 1.2.0 — a new, backward-compatible feature (MINOR, PATCH back to 0). (c) 2.0.0 — a breaking change to the public API (MAJOR), exactly Bazaar Cart's 2.0.0 in versions and builds.
(a) 1.1.1 — چاکسازی (PATCH). (b) 1.2.0 — تایبەتمەندییەکی نوێ کە لەگەڵ پێشوو دەگونجێت (MINOR، PATCH دەگەڕێتەوە بۆ 0). (c) 2.0.0 — لە API گشتییەکەدا (MAJOR)، ڕێک 2.0.0ـی Bazaar Cart لە وەشانەکان و بیلدەکان.
Write the acceptance example for BC-2 that would have caught bug #7, in Given / When / Then form.
ئەو نموونەی پەسەندکردنەی BC-2 بنووسە کە #7ـی دەگرت، بە شێوەی Given / When / Then.
Show answerوەڵامەکە پیشان بدە
Given 3,000 g of honey in stock, when a customer adds honey 2000 twice and confirms, then the order is refused with "Honey: only 3,000 g in stock" and the stock stays 3,000 g. It is exactly the regression test test_r3_two_lines_of_one_product_count_together.
Given 3,000 گرام هەنگوین لە کۆگادا، when کڕیارێک دوو جار honey 2000 زیاد دەکات و پشتڕاستی دەکاتەوە، then داواکارییەکە بە «Honey: only 3,000 g in stock» ڕەت دەکرێتەوە و کۆگا 3,000 گرام دەمێنێتەوە. ئەمە ڕێک : test_r3_two_lines_of_one_product_count_together.
Write a presenter test: after honey 2000, walnuts 2000 and sumac 500, the view's total must read Total 95,000 IQD (delivery 0, discount 5,000).
تاقیکردنەوەیەکی بنووسە: دوای honey 2000، walnuts 2000 و sumac 500، کۆی دەبێت Total 95,000 IQD (delivery 0, discount 5,000) بێت.
Show answerوەڵامەکە پیشان بدە
import unittest
from bazaar_cart.presenter import CartPresenter
from bazaar_cart.repository import ProductRepository, open_demo_db
from bazaar_cart.services import OrderService
from tests.test_presenter import FakeCartView
from tests.test_services import FakeNotifier
class DiscountTextTest(unittest.TestCase):
def test_the_total_shows_the_discount(self):
view = FakeCartView()
presenter = CartPresenter(OrderService(ProductRepository(open_demo_db()), FakeNotifier()), view)
for code, grams in (("honey", "2000"), ("walnuts", "2000"), ("sumac", "500")):
presenter.on_add(code, grams)
self.assertEqual(view.total, "Total 95,000 IQD (delivery 0, discount 5,000)")test_the_total_shows_the_discount (tests.test_discount_text.DiscountTextTest.test_the_total_shows_the_discount) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.002s OK
Add the SmsNotifier from the dependency injection lesson to notifiers.py, and change only app.py so that the shop (with SMS_API_KEY set) sends real messages while every other computer keeps ConsoleNotifier.
SmsNotifierـی وانەی تێخستنی وابەستەیی بۆ notifiers.py زیاد بکە و تەنیا app.py بگۆڕە بە جۆرێک کە دوکانەکە (بە SMS_API_KEYـەوە) نامەی ڕاستەقینە بنێرێت و هەموو تر ConsoleNotifier بهێڵێتەوە.
Show answerوەڵامەکە پیشان بدە
"""Composition root: the one place where Bazaar Cart's objects are made and connected."""
import os
from . import __version__
from .cli import ConsoleCartView
from .notifiers import ConsoleNotifier, SmsNotifier
from .presenter import CartPresenter
from .repository import ProductRepository, open_demo_db
from .services import OrderService
def main():
print(f"Bazaar Cart {__version__} - Soran's shop, Sulaimani bazaar")
repo = ProductRepository(open_demo_db()) # data access layer
key = os.environ.get("SMS_API_KEY")
notifier = SmsNotifier(key) if key else ConsoleNotifier() # real SMS only in the shop
service = OrderService(repo, notifier) # business layer + its notifier
view = ConsoleCartView() # presentation: the view ...
view.run(CartPresenter(service, view)) # ... and its presenter$ python3.13 -m bazaar_cart Bazaar Cart 1.1.0 - Soran's shop, Sulaimani bazaar Add (code grams), empty to stop: tea 250 Black tea 250 g: 3,500 IQD Total 6,500 IQD (delivery 3,000) Add (code grams), empty to stop: Phone for the SMS: 0770 000 0004 [SMS to 0770 000 0004] Bazaar Cart: your order of 6,500 IQD is on its way. Order placed: 6,500 IQD. Thank you!
The business layer, the presenter and the tests do not change — that is what the composition root is for.
، و تاقیکردنەوەکان ناگۆڕێن — بۆ ئەمەیە.
Summary: the whole course in one projectپوختە: هەموو خولەکە لە یەک پڕۆژەدا


- Plan: stories with acceptance examples, MoSCoW, two sprints on a Kanban board, a Definition of Done.
- History: small commits, a feature branch, a resolved conflict, annotated tags.
- Quality: tests first, every layer tested, a regression test for every bug.
- Design: three layers, MVP for the screen, constructor injection and one composition root.
- Releases: 1.0.0, a PATCH 1.0.1 and a MINOR 1.1.0, each with a CHANGELOG section, a tag and a
.pyz.
- پلان: چیرۆک بە نموونەی پەسەندکردن، MoSCoW، دوو لەسەر تەختەی ، .
- مێژوو: بچووک، ، ناکۆکییەکی چارەسەرکراو، تاگی annotated.
- جۆرایەتی: تاقیکردنەوە لە پێشەوە، هەموو تاقیکراوە، بۆ هەموو .
- دیزاین: سێ ، MVP بۆ شاشە، و یەک .
- : 1.0.0، PATCHـی 1.0.1 و MINORـی 1.1.0، هەریەکە بە بەشێکی CHANGELOG، تاگێک و
.pyzـێک.
What next?دواتر چی؟
- Build the same layers on a phone: MVVM in Flutter with Provider and project structure with get_it.
- Build them for the web: a PHP CRUD application, then see how an ORM replaces hand-written SQL.
- Use AI assistants as a careful engineer: the Generative AI course explains how they work, and AI agents and limits and safety show why you still review, test and stay responsible for the code.
- Help an open-source project with a fork and a pull request (forks): real reviewers, real issues, real releases.
- هەمان لەسەر مۆبایل دروست بکە: MVVM لە Flutter بە Provider و ڕێکخستنی پڕۆژە بە get_it.
- بۆ وێب دروستیان بکە: ئەپێکی CRUDـی PHP، پاشان بزانە چۆن ORM جێگەی دەستنووس دەگرێتەوە.
- یاریدەدەری وەک ئەندازیارێکی وریا بەکار بهێنە: خولی ژیریی دەستکردی بەرهەمهێنەر ڕوونی دەکاتەوە چۆن کار دەکەن و بریکارەکانی ژیریی دەستکرد و سنوور و سەلامەتی پیشان دەدەن بۆچی هێشتا پێداچوونەوە و تاقیکردنەوە دەکەیت و بەرپرسی کۆدەکەیت.
- بە و یارمەتیی پڕۆژەیەکی بدە (فۆرکەکان): پێداچووەرەوەی ڕاستەقینە، ڕاستەقینە، ڕاستەقینە.
Lesson complete!وانەکە تەواو بوو!
Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.
Key termsزاراوە سەرەکییەکان
Related lecturesوانە پەیوەندیدارەکان
- What Is Software Engineering?ئەندازیاریی نەرمەواڵە چییە؟
- The Development Life Cycle: Waterfall, Iterative and Agileسووڕی ژیانی گەشەپێدانی نەرمەواڵە: مۆدێلی تاڤگە، خولبەخول و ئەجایڵ
- Requirements and Design: Deciding What to Buildپێداویستییەکان و دیزاین: بڕیاردان لەسەر ئەوەی چی دروست بکەین
- Git Basics: Saving the History of Your Codeبنەماکانی Git: پاشەکەوتکردنی مێژووی کۆدەکەت
- GitHub and Teamwork: Remotes, Pull Requests and IssuesGitHub و کاری تیمی: کۆگاکانی کۆدی دوور، داواکارییەکانی بەیەککردن و ئیشووەکان
- Layers and Business Logic: Where Each Rule Livesچینەکان و لۆجیکی کاروبار: هەر یاسایەک لە کوێ دەژی
- MVC, MVP and MVVM: Three Ways to Organise the ScreenMVC، MVP و MVVM: سێ ڕێگە بۆ ڕێکخستنی شاشە
- MVVM in Flutter with ProviderMVVM لە Flutterـدا بە Provider
- Building a CRUD Application with PHP and MySQLدروستکردنی بەرنامەیەکی CRUD بە PHP و MySQL
References and further readingسەرچاوەکان و خوێندنەوەی زیاتر
- Semantic Versioning 2.0.0
- Keep a Changelog 1.1.0
- Pro Git: Git Basics – Tagging
- GitHub Docs: GitHub flow
- Python documentation: unittest (test discovery, assertRaises, setUp)
- Python documentation: zipapp
- GitHub Docs: Building and testing Python (actions/checkout@v6, actions/setup-python@v5)
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.