From 55034316980913f844841c875726b6540d896f3b Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 16:04:49 -0400 Subject: [PATCH 01/17] Temporarily removed 'README.md' so that it can not be used as context, wrote specs for the transaction endpoints + tests for fraud alerts. Also added .claude/settings.json to create gaurdrails to keep claude within a defined set of behavior that I use for all my projects. --- .claude/settings.json | 20 ++++++++++++++++++ README.md | 43 --------------------------------------- SPECS/feature-template.md | 14 ------------- SPECS/transactions.md | 31 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 57 deletions(-) create mode 100644 .claude/settings.json delete mode 100644 README.md delete mode 100644 SPECS/feature-template.md create mode 100644 SPECS/transactions.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..e8cc4f0b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(*)", + "Edit(*)", + "Write(*)", + "Read(*)", + "Glob(*)", + "Grep(*)", + "Task(*)", + "WebFetch(*)", + "WebSearch(*)", + "TodoWrite(*)", + "NotebookEdit(*)" + ], + "deny": [ + "Bash(git *)" + ] + } +} diff --git a/README.md b/README.md deleted file mode 100644 index 494f1c75..00000000 --- a/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Candidate Assessment: Spec-Driven Development With Codegen Tools - -This assessment evaluates how you use modern code generation tools (for example `5.2-Codex`, `Claude`, `Copilot`, and similar) to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both. - -## Goals -- Build a working application with at least one meaningful feature. -- Create a testing framework to validate the application. -- Demonstrate effective use of code generation tools to accelerate delivery. -- Show clear, maintainable engineering practices. - -## Deliverables -- Application source code in this repository. -- A test suite and test harness that can be run locally. -- Documentation that explains how to run the app and the tests. - -## Scope Options -Pick one: -- Frontend-only application. -- Backend-only application. -- Full-stack application. - -Your solution should include at least one real workflow, for example: -- Create and view a resource. -- Search or filter data. -- Persist data in memory or storage. - -## Rules -- You must use a code generation tool (for example `5.2-Codex`, `Claude`, or similar). You can use multiple tools. -- You must build the application and a testing framework for it. -- The application and tests must run locally. -- Do not include secrets or credentials in this repository. - -## Evaluation Criteria -- Working product: Does the app do what it claims? -- Test coverage: Do tests cover key workflows and edge cases? -- Engineering quality: Clarity, structure, and maintainability. -- Use of codegen: How effectively you used tools to accelerate work. -- Documentation: Clear setup and run instructions. - -## What to Submit -- When you are complete, put up a Pull Request against this repository with your changes. -- A short summary of your approach and tools used in your PR submission -- Any additional information or approach that helped you. diff --git a/SPECS/feature-template.md b/SPECS/feature-template.md deleted file mode 100644 index 7dbc70a5..00000000 --- a/SPECS/feature-template.md +++ /dev/null @@ -1,14 +0,0 @@ -# Feature Spec: - -## Goal -- - -## Scope -- In: -- Out: - -## Requirements -- - -## Acceptance Criteria -- [ ] \ No newline at end of file diff --git a/SPECS/transactions.md b/SPECS/transactions.md new file mode 100644 index 00000000..a27db25b --- /dev/null +++ b/SPECS/transactions.md @@ -0,0 +1,31 @@ +# Feature Spec: Transactions + +## Goal +- Provide CRUD endpoints for transaction records that represent raw financial transaction data flagged upstream by Ally's fraud detection AI. + +## Scope +- In: Creating transactions, retrieving transactions by ID, field validation, PII-sensitive field storage +- Out: Updating or deleting transactions (transactions are immutable records of what occurred), bulk import, transaction search/listing (transactions are accessed individually or via their linked alerts) + +## Requirements +- Each transaction must have a unique `id` (UUID, server-generated) +- Required fields on creation: `amount`, `merchant_name`, `merchant_category`, `location`, `timestamp`, `card_id`, `account_id` +- `amount` must be a positive number greater than 0 +- `merchant_category` must be one of a defined enum: `electronics`, `travel`, `groceries`, `gas_station`, `restaurant`, `entertainment`, `healthcare`, `utilities`, `cash_advance`, `other` +- `location` is a string (e.g., "Charlotte, NC") +- `timestamp` must be a valid ISO 8601 datetime string +- `card_id` and `account_id` are stored in full but treated as PII-sensitive (see pii-masking spec) +- Transactions are immutable after creation, so no update or delete endpoints +- GET response returns the transaction with PII fields masked by default + +## Acceptance Criteria +- [ ] POST /transactions creates a transaction and returns it with a generated UUID +- [ ] POST /transactions returns 422 for missing required fields +- [ ] POST /transactions returns 422 for amount <= 0 +- [ ] POST /transactions returns 422 for invalid merchant_category +- [ ] POST /transactions returns 422 for invalid timestamp format +- [ ] GET /transactions/{id} returns the transaction with PII fields masked +- [ ] GET /transactions/{id} returns 404 for nonexistent ID +- [ ] POST /transactions accepts and stores all valid merchant_category values +- [ ] Response includes server-generated `id` that was not provided in the request body +- [ ] Extra/unknown fields in the request body are ignored or rejected (pick one, document it) \ No newline at end of file From 9d5d34814a8d85a1253c6d9583c7a28fca5ecd97 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 16:29:59 -0400 Subject: [PATCH 02/17] Added specs for pii masking functionality, which is necessary to properly define how transactions will work. I will fully implement pii masking later on in the project. --- SPECS/pii-masking.md | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 SPECS/pii-masking.md diff --git a/SPECS/pii-masking.md b/SPECS/pii-masking.md new file mode 100644 index 00000000..f9c94409 --- /dev/null +++ b/SPECS/pii-masking.md @@ -0,0 +1,61 @@ +# Feature Spec: PII Masking + +## Goal +- Ensure PII-sensitive fields are masked by default in all API responses, reflecting Ally Financial's commitment to responsible data handling. Ally contributed a PII Masking module to LangChain's open-source ecosystem specifically because customer interactions always involve PII — this feature demonstrates awareness of that principle. + +## Scope +- In: Masking card_id and account_id in API responses, an authorized access flag to reveal full values, consistent masking across all endpoints that return transaction data +- Out: Encryption at rest, role-based access control, authentication/authorization (this is a demonstration of the concept, not a production auth system) + +## Requirements + +### Masked Fields +- `card_id` and `account_id` are the PII-sensitive fields +- When masked, only the last 4 characters are visible, prefixed with asterisks: `****1234` +- If the value is 4 characters or fewer, mask the entire value: `****` +- Masking is applied at the API response layer — storage retains the full value + +### Default Behavior +- All API responses that include transaction data mask PII fields by default +- This applies to: + - GET /transactions/{id} + - POST /transactions (response body) + - GET /alerts/{id} (embedded transaction data) + - GET /alerts (embedded transaction data in each alert) + +### Authorized Access +- A query parameter `show_pii=true` reveals the unmasked values +- This simulates an authorized analyst session — in production this would be gated by role-based auth +- When `show_pii=true`, card_id and account_id are returned in full +- When `show_pii` is absent or `false`, fields are masked + +### contains_pii Flag +- Each alert has a `contains_pii` boolean +- Set to `true` by default since linked transactions always contain card_id and account_id +- This flag is informational — it does not control masking behavior (masking always applies regardless) + +## Acceptance Criteria + +### Default Masking +- [ ] GET /transactions/{id} returns card_id and account_id masked (e.g., "****5678") +- [ ] POST /transactions response body returns masked PII fields +- [ ] GET /alerts/{id} returns embedded transaction data with masked PII +- [ ] GET /alerts list returns all embedded transaction data with masked PII +- [ ] Masking shows last 4 characters: "1234567890" → "****7890" +- [ ] Values with 4 or fewer characters are fully masked: "1234" → "****" + +### Authorized Access +- [ ] GET /transactions/{id}?show_pii=true returns full card_id and account_id +- [ ] GET /alerts/{id}?show_pii=true returns full PII in embedded transaction +- [ ] GET /alerts?show_pii=true returns full PII across all results +- [ ] show_pii=false behaves the same as omitting the parameter (masked) + +### Consistency +- [ ] PII masking is applied consistently across all endpoints — no endpoint leaks unmasked data by default +- [ ] Masking does not affect stored data — full values are preserved in the database +- [ ] The contains_pii flag on alerts is set to true by default + +### Edge Cases +- [ ] Empty string card_id or account_id is masked as "****" +- [ ] Very long PII values are correctly masked (only last 4 shown) +- [ ] show_pii parameter with non-boolean values (e.g., "yes", "1") is handled gracefully (treat as false or return 422) \ No newline at end of file From 898509fc202c7a466adb701fcd8b78d6672eb455 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 17:08:59 -0400 Subject: [PATCH 03/17] Implemented the transactions feature. Wrote tests, ensured that all tests passed. --- fraud-alert-service/fraud_alerts.db | Bin 0 -> 20480 bytes fraud-alert-service/requirements.txt | 7 ++ fraud-alert-service/src/__init__.py | 0 .../src/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 201 bytes .../src/__pycache__/database.cpython-310.pyc | Bin 0 -> 1448 bytes .../src/__pycache__/main.cpython-310.pyc | Bin 0 -> 642 bytes .../src/__pycache__/models.cpython-310.pyc | Bin 0 -> 1768 bytes fraud-alert-service/src/database.py | 40 ++++++ fraud-alert-service/src/main.py | 17 +++ fraud-alert-service/src/models.py | 48 ++++++++ fraud-alert-service/src/routes/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 208 bytes .../__pycache__/transactions.cpython-310.pyc | Bin 0 -> 1941 bytes .../src/routes/transactions.py | 61 ++++++++++ fraud-alert-service/tests/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 203 bytes .../conftest.cpython-310-pytest-8.3.3.pyc | Bin 0 -> 840 bytes ..._transactions.cpython-310-pytest-8.3.3.pyc | Bin 0 -> 7453 bytes fraud-alert-service/tests/conftest.py | 23 ++++ .../tests/test_transactions.py | 115 ++++++++++++++++++ 20 files changed, 311 insertions(+) create mode 100644 fraud-alert-service/fraud_alerts.db create mode 100644 fraud-alert-service/requirements.txt create mode 100644 fraud-alert-service/src/__init__.py create mode 100644 fraud-alert-service/src/__pycache__/__init__.cpython-310.pyc create mode 100644 fraud-alert-service/src/__pycache__/database.cpython-310.pyc create mode 100644 fraud-alert-service/src/__pycache__/main.cpython-310.pyc create mode 100644 fraud-alert-service/src/__pycache__/models.cpython-310.pyc create mode 100644 fraud-alert-service/src/database.py create mode 100644 fraud-alert-service/src/main.py create mode 100644 fraud-alert-service/src/models.py create mode 100644 fraud-alert-service/src/routes/__init__.py create mode 100644 fraud-alert-service/src/routes/__pycache__/__init__.cpython-310.pyc create mode 100644 fraud-alert-service/src/routes/__pycache__/transactions.cpython-310.pyc create mode 100644 fraud-alert-service/src/routes/transactions.py create mode 100644 fraud-alert-service/tests/__init__.py create mode 100644 fraud-alert-service/tests/__pycache__/__init__.cpython-310.pyc create mode 100644 fraud-alert-service/tests/__pycache__/conftest.cpython-310-pytest-8.3.3.pyc create mode 100644 fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc create mode 100644 fraud-alert-service/tests/conftest.py create mode 100644 fraud-alert-service/tests/test_transactions.py diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db new file mode 100644 index 0000000000000000000000000000000000000000..9236035374497db590e8b8be6e7f43520b65dd6e GIT binary patch literal 20480 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 GMDuT=HS^>E literal 0 HcmV?d00001 diff --git a/fraud-alert-service/requirements.txt b/fraud-alert-service/requirements.txt new file mode 100644 index 00000000..89e5cf77 --- /dev/null +++ b/fraud-alert-service/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.115.0 +uvicorn==0.30.6 +pydantic==2.9.2 +httpx==0.27.2 +pytest==8.3.3 +pytest-asyncio==0.24.0 +aiosqlite==0.20.0 diff --git a/fraud-alert-service/src/__init__.py b/fraud-alert-service/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fraud-alert-service/src/__pycache__/__init__.cpython-310.pyc b/fraud-alert-service/src/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0bcca3ff98b80e4097f55d8fd3fc5cc3c98517c GIT binary patch literal 201 zcmd1j<>g`kg83WvW`gL)AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yY#5-bb= DR(>|r literal 0 HcmV?d00001 diff --git a/fraud-alert-service/src/__pycache__/database.cpython-310.pyc b/fraud-alert-service/src/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08c44ad1a4b706911706ccebf2749c781691a181 GIT binary patch literal 1448 zcmah}&2AGh5VoDo?j}tOh#w&;q!kB5B+>|R;eZgO1W{2!)dpzyvT|&vY}lXRb(*#c z5>W2E0zLKwFMz~z_{xb_;6NE~QfO(V*z(vj{ycv>JL&Xv8NvAe^;>w|Lg>5k+4K@7kBNh&8=DFJHqK;UKAF$1?svTi#y!? zSoBMR9Nu&YFY)q6c;%N5W!&Z%uv5GO*eQ-zQFZz!SVzq&)-xI{x0o#dS$}g;>1Aj`UgP=p1O6B%Gk&ry3ErZxcatfGVQT=%) zWL8hW3z~E(OWRrfp~zmVv{%o1g4MVTH$_t8VzZ`1rv4X-DDA}}QT05wS{_>sRBnb$ z)HBKIoGRL;nV9Qs>zNIqg0X{Pnz$x18%P6vL#0cN zEhc&j9Q0k8Mp2ux*Sg4}6v8S2KF{2URDjilw`X9_zY%%o5)0e70@K9=li%dvQc<3R z`%@#!Kkgm49mVx&<}EhyaJ+|2L>yT#cMBuGZ=vks0OV$_k7KElCjA0sQ}&LGg_Xv9 ztKqfgA1pPz#YbNAS<7pzEv~j!Jtb+9K|w%tnRnyZE2ALf|J1Y^Yb|eiW%23!%DVTY zv3~Oen#O5AQKJ`Naw#{|e7>}F5*iE1pu$v;&{%l6YU9-eFr!LrKrekQwG!(;`-l5$f=!B670&VxGg#T1K4<7`}^R36a xyfEq%khZ-3bBA)>M+)3D37J>9dpAybKN9y$kIK#hagnL=v*dDt*cb4X${*(WNT>h+ literal 0 HcmV?d00001 diff --git a/fraud-alert-service/src/__pycache__/main.cpython-310.pyc b/fraud-alert-service/src/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f9b84aa467c8e38e75bbe65c6d27068ab97ac7e GIT binary patch literal 642 zcmZ8e&2H2%5VoDo&!%Zr(E~TM*Br9p0T7}BRpL@1RrC-cRkQIdH8@VNy{ncJs9bpm zIQGZ`@FKZ#;uSbC&H}2K$m5yM@q9C%`~Cf#kv;tOUA^Os{Rqijn^E$FmU~4`GRZaT zc*A*!g%(}ZL{l8;Sr<3)6wh?hrA@{eav5W^7BZ3Pr`Zi}<`~Hg=W_m8G&$y9_AvVz zYUO9F-1|uktmNYnxNFn4z43VEJ23DXEy3vtIDh=&`7MRYD1RYWgeJD%c(h{#!_d+I z|188|Jr&gaJqkVw&u$2k?0oV>$xm&VujDqR?Uvb$wqKX^ONZ9g2K*(MH_-MgS3g7d z*7rm025c*7)fP=9aa(zG{(lg4KXhn(y|%EC6=<}+qLN$HV(n~Occ9F2xE?dD*60Q> zbK-RgIq@M8Ik(|W-aj}A*B8g(`r;h4lHgTu7H7Lkr5IzcJe`KWPs?J=|7fPw%F@(q zOxGkC49Z3!D~NhYHDCqQOiWO7KR}a$Zs{!;2W_BmHuTaQ-Gwf8LQiCqT;qWrd ldlJN(6x_Km)0jQ#db!c~I6Q?rBv-IP(8on|$cy+u{095Wr6m9W literal 0 HcmV?d00001 diff --git a/fraud-alert-service/src/__pycache__/models.cpython-310.pyc b/fraud-alert-service/src/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3815b45b3d42a4162cf9bab66ff952c6f8c43185 GIT binary patch literal 1768 zcmZ`)&2k(y5FX9n&dlz{b{t5c2rO`bh1x6!jw#B89Via*0WgQTOvzHOcar&8TAD4K z%8AXD2jP|%;6-%h#4B*3d)AI!cF>l-?v_TaR`(}uJ052qwb!rzD(lem{$}HPi^w>j zZjeBGT2x-`<(^>OuSD(Vey;~rP=|Tg>tPkuaUR!$d{8HOQm1(;yzf07>G+kWW5~eY zg!zz020EdU1maDQkH}7SMt0WQV?F%In~wgWeZ8r0iIx^@Sp(_elcuX#o}Bz}G!0$) ztu*ko)lj+Yux%FQ*-@#ii@qzN(r#zLqFBqSBp)p*_ISMsG7hMluL;1*h4yk^i#*VN z9_k>EbeP9F$_F~m6FtaNo#dHL^P$f2(KbrA`xHE!p zn!-!ZuvLI17&knVrZ868vTa<3Kx&8Nz>T4?fL4}GO`6M=AS=6662YaNEvwS<@{y8e zSxCK>jRF_7b_sYq9lN9`8d*b8xU4AZR(BQaqoVk=lht+xx#T!x7%_|)?l9~y>@w^z z+-2A&_|wDttTX)kY<^+@&AgG84*X22_SDRez&y9@YHn7bW*W;iG&2qBnWb~x9E7S} z@zLfBl%1Z*icUKtm+Ml&+@P9M*ie~At8+|g!-Tqdw^u@O^OuPJn_D%F>H7m-*brRS z^u&We3+?|Q@(?13b@14O0m+agu~0`Bc1$uQdvIYV7cwQ8>0l#(Y7dbfLbM3=NRMBI z8?h0@hC9UB9k=&0RnkbswLL^2O1OB@;%TXgwGsSAY;dux+m6d!UofYyo7;P1s!Jv% zRZBY4B;^Z0M5$M9K!ngmsa+-+?S1Y?uXRPJ%X1)i{}CpOP6>f zeittVg&iCt--H(rw}DOS&e+K*OwRfP;DoC^`HBvZ{UWOlo*=e3^G&x}BnsP19~Kn4vom7>hkwkaK}dD>qb?G^Pf-Y&Cwy!*6)L)%Ya%9szG@*vh<2 zBz_{UezEV1_eKBJEglB6;((VP6I|yZgjx`rB9cDInBnofe{ZXhOCE2Zx^I}lhTpQ0H=+n0_;-Wd@xA^30GQjOx&QzG literal 0 HcmV?d00001 diff --git a/fraud-alert-service/src/database.py b/fraud-alert-service/src/database.py new file mode 100644 index 00000000..f1a5864a --- /dev/null +++ b/fraud-alert-service/src/database.py @@ -0,0 +1,40 @@ +import sqlite3 +from contextlib import contextmanager +from pathlib import Path + +DB_PATH = Path(__file__).parent.parent / "fraud_alerts.db" + + +def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection: + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + return conn + + +@contextmanager +def db(db_path: Path = DB_PATH): + conn = get_connection(db_path) + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + + +def init_db(db_path: Path = DB_PATH) -> None: + with db(db_path) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS transactions ( + id TEXT PRIMARY KEY, + amount REAL NOT NULL, + merchant_name TEXT NOT NULL, + merchant_category TEXT NOT NULL, + location TEXT NOT NULL, + timestamp TEXT NOT NULL, + card_id TEXT NOT NULL, + account_id TEXT NOT NULL + ) + """) diff --git a/fraud-alert-service/src/main.py b/fraud-alert-service/src/main.py new file mode 100644 index 00000000..a42211bf --- /dev/null +++ b/fraud-alert-service/src/main.py @@ -0,0 +1,17 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from src.database import init_db +from src.routes.transactions import router as transactions_router + + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + yield + + +app = FastAPI(title="Fraud Alert Validation Service", lifespan=lifespan) + +app.include_router(transactions_router) diff --git a/fraud-alert-service/src/models.py b/fraud-alert-service/src/models.py new file mode 100644 index 00000000..f82d10f4 --- /dev/null +++ b/fraud-alert-service/src/models.py @@ -0,0 +1,48 @@ +from datetime import datetime +from enum import Enum +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class MerchantCategory(str, Enum): + electronics = "electronics" + travel = "travel" + groceries = "groceries" + gas_station = "gas_station" + restaurant = "restaurant" + entertainment = "entertainment" + healthcare = "healthcare" + utilities = "utilities" + cash_advance = "cash_advance" + other = "other" + + +class TransactionCreate(BaseModel): + model_config = ConfigDict(extra="forbid") + + amount: float + merchant_name: str + merchant_category: MerchantCategory + location: str + timestamp: datetime + card_id: str + account_id: str + + @field_validator("amount") + @classmethod + def amount_must_be_positive(cls, v: float) -> float: + if v <= 0: + raise ValueError("amount must be greater than 0") + return v + + +class TransactionResponse(BaseModel): + id: UUID + amount: float + merchant_name: str + merchant_category: MerchantCategory + location: str + timestamp: datetime + card_id: str + account_id: str diff --git a/fraud-alert-service/src/routes/__init__.py b/fraud-alert-service/src/routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fraud-alert-service/src/routes/__pycache__/__init__.cpython-310.pyc b/fraud-alert-service/src/routes/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..637d4a3061c455ac4fdd086c53843ff4b0f3bf58 GIT binary patch literal 208 zcmZ9GF$w}P5Jj`Hg$R2Pi#Qh$v9R7otV2vjjP533CcEHmZ0)>&7qRsgRwk9=hyRB8 zFrNucGZR_Y`@@y_D)?2EmvuJR8CGn?ylQWPO8mu}VaG@eUZCvC!>VS{{5Ap%D< z(vlm|ON*mS$mu`CF2o*vf&=NnO5GtPnJY(U5SR>581fw`<-AKuE#l=lUG~8a9j?kV IJjtlV2L!JE^=Zafy4nQRWNF#;zo%}#a^t<&N$hyAJ*(R zU)YzV*N6)zPRWrQ{{Vjg{{TpH|({8IqsOOWhm0BTSa8J=o!gktPrr21xL#>69W!7FH;M8&w|m2|%lS#jAb z$F^r9gArP!rENs5Q|wh}nb`wOZCZIoyeg~g)@YU4v_|WvCY@nr7;DhxGjf8wI*iWH zSs0x=#dMxEU~GXd>aiw8n}&PqIjqm~nJjW9 zMm!0STF>R9`YO$S!juhjrHvz_uoX+%an(EELo;(Z&|=XUN^ER9mXsX?E&{kblHy>7t0WGFAP{a<3XFN!_216Vr6(J z8+}+sU!;A0d0uJ{$zbcWB36Es1>JTZQ3P4F0>5sJoWZ0r_5iXf$33$P4gpH6B%M=Y@PtaKLY0LBV?%eJCG~8Yj%(Rw4L0u z*4^xE^qjSB@3E7o>Xei6zZcS$bHSvRb4?w^q?~%iq?U8Vq?U8dq?U8ZB#3jvq^XI% zeemc>XLHJ==f2-^-tcp$9AAF0*?H7i?KvMhUvE7AW@kY*;h zB%=|qx}fl5u1c3Iu>%&UA~4NsS=jTV5xb$cEQeV-NO|mw8>UNZz(lZ{ zbXmxFQ8}ur2lZY5r(Dz#GE9MX>S&}1YvXS#H#KjL2&xis%?9f;h&GDY7wZ2YQ-#Pf Vs;GghxPj`pZq{oJRJ}cq{s%i9-q8R6 literal 0 HcmV?d00001 diff --git a/fraud-alert-service/src/routes/transactions.py b/fraud-alert-service/src/routes/transactions.py new file mode 100644 index 00000000..115b8cfe --- /dev/null +++ b/fraud-alert-service/src/routes/transactions.py @@ -0,0 +1,61 @@ +import uuid +from pathlib import Path + +from fastapi import APIRouter, HTTPException + +from src.database import db +from src.models import TransactionCreate, TransactionResponse + +router = APIRouter(prefix="/transactions", tags=["transactions"]) + + +def _row_to_response(row) -> TransactionResponse: + return TransactionResponse( + id=row["id"], + amount=row["amount"], + merchant_name=row["merchant_name"], + merchant_category=row["merchant_category"], + location=row["location"], + timestamp=row["timestamp"], + card_id=row["card_id"], + account_id=row["account_id"], + ) + + +@router.post("", response_model=TransactionResponse, status_code=201) +def create_transaction(body: TransactionCreate): + transaction_id = str(uuid.uuid4()) + with db() as conn: + conn.execute( + """ + INSERT INTO transactions + (id, amount, merchant_name, merchant_category, location, timestamp, card_id, account_id) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + transaction_id, + body.amount, + body.merchant_name, + body.merchant_category.value, + body.location, + body.timestamp.isoformat(), + body.card_id, + body.account_id, + ), + ) + row = conn.execute( + "SELECT * FROM transactions WHERE id = ?", (transaction_id,) + ).fetchone() + return _row_to_response(row) + + +@router.get("/{transaction_id}", response_model=TransactionResponse) +def get_transaction(transaction_id: str): + with db() as conn: + row = conn.execute( + "SELECT * FROM transactions WHERE id = ?", (transaction_id,) + ).fetchone() + if row is None: + raise HTTPException(status_code=404, detail="Transaction not found") + return _row_to_response(row) diff --git a/fraud-alert-service/tests/__init__.py b/fraud-alert-service/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fraud-alert-service/tests/__pycache__/__init__.cpython-310.pyc b/fraud-alert-service/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43cdc2b337ff2a7eac10ed3b6d4fed437a353ce1 GIT binary patch literal 203 zcmd1j<>g`kf~6bwW`gL)AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yY=N{m|mnqGJ8L z#FC7}ysX6J{G?)im(=3ylKcYw;)2v<-ISutveZ1?l+-falGNf7bive|{DR!nyb}Gi zqQue^-Nc;Kq7q%8k!6|5srn!d#rpB_nR%Hd@$q^EmA5!-a`RJ4b5iX(70aq2-Rb;DWz~p|I=`pxRJ(K+ zBhg=sdCZfqXoKlpj$fla@*BFN!#(VxBW6EusMRD}}_?7naXiux|f!d24 zu~l2N0E;GkwslCO<$GB(<#72O={_X>sq;5RXp`5F$?63wo0-WEg!yQjHaD#(CtS-@ zQBSxyo%lii4+_<^74VhMHCyrtQ$pJbJn~eQBKHL*FPnPaVQ4R0Tz2ec(A$hb1)oU} z2Qel%4G4aCnTC4c0o-$Bwv@`s+PG}Ly(n%@_lx$Tdm1y)#>%FiYH_Be6|R31a>;cp zg<_vxFb$lDs+~(ET;iEga;5`l=HhJ%9nYDutd&z=upPAHlx9Yk(`)W2x%PAn)gASe w_>79ly5rv^b9ru;T6ADKrf(_#Z#eRF`wE}=ud1WsG0HFrGMstAQMiNu0j^uxzW@LL literal 0 HcmV?d00001 diff --git a/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85021664e02ef3ac3e3e7a71696a949a1bfdb71d GIT binary patch literal 7453 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 Date: Fri, 20 Mar 2026 17:15:01 -0400 Subject: [PATCH 04/17] Added alert specs, updated todo to reflect what needs to be done. --- SPECS/alerts.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ TODO.md | 13 +++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 SPECS/alerts.md diff --git a/SPECS/alerts.md b/SPECS/alerts.md new file mode 100644 index 00000000..b0af4df6 --- /dev/null +++ b/SPECS/alerts.md @@ -0,0 +1,48 @@ +# Feature Spec: Alerts + +## Goal +- Provide endpoints for creating and retrieving fraud alerts that wrap a flagged transaction with risk assessment data and lifecycle tracking. + +## Scope +- In: Creating alerts linked to transactions, retrieving alerts by ID, risk score validation, automatic risk level derivation, timestamp generation +- Out: Alert status transitions (see state-machine spec), alert filtering/listing (see filtering spec), summary statistics (see summary-stats spec) + +## Requirements +- Each alert must have a unique `id` (UUID, server-generated) +- Required fields on creation: `transaction_id`, `risk_score` +- `transaction_id` must reference an existing transaction +- A transaction can only have one alert (no duplicate alerts for the same transaction) +- `risk_score` must be a float between 0.0 and 1.0 inclusive +- `risk_level` is automatically derived from `risk_score` — never provided by the client: + - `low`: 0.0 <= score < 0.3 + - `medium`: 0.3 <= score < 0.6 + - `high`: 0.6 <= score < 0.8 + - `critical`: 0.8 <= score <= 1.0 +- `status` is initialized to `pending` on creation +- `analyst_id` is null on creation +- `contains_pii` is set to `true` by default (since the linked transaction contains card_id and account_id) +- `created_at` is server-generated at creation time +- `updated_at` is server-generated and updated on any modification +- `status_history` is initialized with one entry: `{status: "pending", timestamp: , changed_by: "system"}` +- GET response returns the alert with its linked transaction's PII fields masked by default + +## Acceptance Criteria +- [ ] POST /alerts creates an alert and returns it with generated UUID, pending status, and derived risk_level +- [ ] POST /alerts returns 422 for missing transaction_id or risk_score +- [ ] POST /alerts returns 404 if transaction_id does not reference an existing transaction +- [ ] POST /alerts returns 409 if an alert already exists for the given transaction_id +- [ ] POST /alerts returns 422 for risk_score < 0.0 +- [ ] POST /alerts returns 422 for risk_score > 1.0 +- [ ] POST /alerts returns 422 for non-numeric risk_score +- [ ] Risk level is correctly derived at boundary: score 0.0 → low +- [ ] Risk level is correctly derived at boundary: score 0.29 → low +- [ ] Risk level is correctly derived at boundary: score 0.3 → medium +- [ ] Risk level is correctly derived at boundary: score 0.59 → medium +- [ ] Risk level is correctly derived at boundary: score 0.6 → high +- [ ] Risk level is correctly derived at boundary: score 0.79 → high +- [ ] Risk level is correctly derived at boundary: score 0.8 → critical +- [ ] Risk level is correctly derived at boundary: score 1.0 → critical +- [ ] Alert is created with status "pending" and a single status_history entry +- [ ] GET /alerts/{id} returns the alert with all fields populated +- [ ] GET /alerts/{id} returns 404 for nonexistent alert ID +- [ ] Client cannot override risk_level, status, or created_at on creation \ No newline at end of file diff --git a/TODO.md b/TODO.md index b5d82042..c7763fbe 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,16 @@ # TODO ## Refactor Proposals -- +- ## New Feature Proposals -- \ No newline at end of file +- + +## In Progress + +### Alerts (SPECS/alerts.md) +- [ ] Add Alert + StatusHistoryEntry Pydantic models to `src/models.py` +- [ ] Add `alerts` table to `src/database.py` (with `status_history` stored as JSON) +- [ ] Add `src/routes/alerts.py` with `POST /alerts` and `GET /alerts/{id}` +- [ ] Register alerts router in `src/main.py` +- [ ] Write tests covering all acceptance criteria in `tests/test_alerts.py` \ No newline at end of file From 441b5d550f2989a00bf5072a8bb7662f28d12e20 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 17:24:48 -0400 Subject: [PATCH 05/17] Implemented alerts functionality. Created and ensured that tests passed. --- TODO.md | 8 - fraud-alert-service/fraud_alerts.db | Bin 20480 -> 65536 bytes .../src/__pycache__/database.cpython-310.pyc | Bin 1448 -> 2111 bytes .../src/__pycache__/main.cpython-310.pyc | Bin 642 -> 702 bytes .../src/__pycache__/models.cpython-310.pyc | Bin 1768 -> 3705 bytes fraud-alert-service/src/database.py | 15 ++ fraud-alert-service/src/main.py | 2 + fraud-alert-service/src/models.py | 60 +++++ .../routes/__pycache__/alerts.cpython-310.pyc | Bin 0 -> 3049 bytes fraud-alert-service/src/routes/alerts.py | 102 +++++++++ .../test_alerts.cpython-310-pytest-8.3.3.pyc | Bin 0 -> 11715 bytes fraud-alert-service/tests/test_alerts.py | 207 ++++++++++++++++++ 12 files changed, 386 insertions(+), 8 deletions(-) create mode 100644 fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc create mode 100644 fraud-alert-service/src/routes/alerts.py create mode 100644 fraud-alert-service/tests/__pycache__/test_alerts.cpython-310-pytest-8.3.3.pyc create mode 100644 fraud-alert-service/tests/test_alerts.py diff --git a/TODO.md b/TODO.md index c7763fbe..972553a5 100644 --- a/TODO.md +++ b/TODO.md @@ -6,11 +6,3 @@ ## New Feature Proposals - -## In Progress - -### Alerts (SPECS/alerts.md) -- [ ] Add Alert + StatusHistoryEntry Pydantic models to `src/models.py` -- [ ] Add `alerts` table to `src/database.py` (with `status_history` stored as JSON) -- [ ] Add `src/routes/alerts.py` with `POST /alerts` and `GET /alerts/{id}` -- [ ] Register alerts router in `src/main.py` -- [ ] Write tests covering all acceptance criteria in `tests/test_alerts.py` \ No newline at end of file diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db index 9236035374497db590e8b8be6e7f43520b65dd6e..1766be290b13358f2cfad9b7b66e68824b1efbee 100644 GIT binary patch literal 65536 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 delta 100 zcmdllu!5T}pO=@50SFFk*qf=wvXSpHlZXY7Tf$Jo(9GD+SIbz#xPUQ*aq@pAadFXd xMn(n@j0EBohF}IwCcnh>jFTU-Xl)K>ZfBgF$!^5O2Gq^P$it}gfAVtn833Ye7f}EJ diff --git a/fraud-alert-service/src/__pycache__/main.cpython-310.pyc b/fraud-alert-service/src/__pycache__/main.cpython-310.pyc index 7f9b84aa467c8e38e75bbe65c6d27068ab97ac7e..bbbaa4c9278cee7c2a580a22ad57aa26d5c86961 100644 GIT binary patch delta 171 zcmZo--N(wC&&$ij00ibc_hz1)$SccOG*R2wB84G^C5JPYJ&KEwA(b(OHI+GyDTOVC zy@jQjF^W5tIfWyYJB72CDT*hRX8~_2*Fr|1Iv{If13M!l*W?aHXGXrsXBdsu1dEH3 z^@{RKOHzyV5_3|EN{WlvfVM^PLOAhYsiMjM7$qmmFa=470OfcXc$k2Yk%y6qS%8s; Jm5Ygw5db!^C?@~_ delta 112 zcmdnT+QiD6&&$ij00agb_huGOtftkca?~!Nb7A1cZ!C L%mR!&tb9xWq$3sJ diff --git a/fraud-alert-service/src/__pycache__/models.cpython-310.pyc b/fraud-alert-service/src/__pycache__/models.cpython-310.pyc index 3815b45b3d42a4162cf9bab66ff952c6f8c43185..1f324ab0129a0e20d2fa00c2a0d469b899577714 100644 GIT binary patch literal 3705 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 Xzp>dI_J&)Voz3>p8@j{(-TwapDbXh` delta 644 zcmZvZK}*~~6vs2kBs<9_i)>d{VQsagXev^AwAvyR@#dw_LM}_XGtj`!W_gnodh%eu zfXt;|!AlW(7Wz5z3GA)lskgot#fq3=e*E8?$9w;o{2g5;y)sE62fy?4i$e3$-iX2J z)~m7ZWF0Lsp~Ea>&Y>fH8GLhOpd#g-@T`lOjue;?Dmd}77~@bz7)NcK$nJY5jek?; zlr=);m8uG@Q1gRXt$*&YUB+p!HSTtJ=D-j&4DAnh{yan}aGRx8aCUzQ2BF>W4(vzo zJ4@{!@7FtQ9DT^AwK@POA$1#XGBxn5ZF-AkRLwBgWj(9Lx;EALlN!%UQ&hzlwPvsQ zn}q=tFA``t1a{wl`I-!YOCVcKcQVb5(W?4fO4y+EwfS2`b6^FrXwUq0wt`+)?WI3E z9?`)n;W6O}VU4g(c#1emL%8RG)7c9`O4uU&cl6NcZ7kWuZTg7F5$t9iw<2ah1xIPG zy;H*#{t)mm)$qD5B+$S{%)iQfRuvQQF@%87A None: account_id TEXT NOT NULL ) """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS alerts ( + id TEXT PRIMARY KEY, + transaction_id TEXT NOT NULL UNIQUE, + risk_score REAL NOT NULL, + risk_level TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + analyst_id TEXT, + contains_pii INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + status_history TEXT NOT NULL, + FOREIGN KEY (transaction_id) REFERENCES transactions(id) + ) + """) diff --git a/fraud-alert-service/src/main.py b/fraud-alert-service/src/main.py index a42211bf..7e890895 100644 --- a/fraud-alert-service/src/main.py +++ b/fraud-alert-service/src/main.py @@ -3,6 +3,7 @@ from fastapi import FastAPI from src.database import init_db +from src.routes.alerts import router as alerts_router from src.routes.transactions import router as transactions_router @@ -15,3 +16,4 @@ async def lifespan(app: FastAPI): app = FastAPI(title="Fraud Alert Validation Service", lifespan=lifespan) app.include_router(transactions_router) +app.include_router(alerts_router) diff --git a/fraud-alert-service/src/models.py b/fraud-alert-service/src/models.py index f82d10f4..ea642170 100644 --- a/fraud-alert-service/src/models.py +++ b/fraud-alert-service/src/models.py @@ -1,5 +1,6 @@ from datetime import datetime from enum import Enum +from typing import Optional from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -46,3 +47,62 @@ class TransactionResponse(BaseModel): timestamp: datetime card_id: str account_id: str + + +class RiskLevel(str, Enum): + low = "low" + medium = "medium" + high = "high" + critical = "critical" + + +class AlertStatus(str, Enum): + pending = "pending" + under_review = "under_review" + confirmed_fraud = "confirmed_fraud" + false_positive = "false_positive" + escalated = "escalated" + + +class StatusHistoryEntry(BaseModel): + status: AlertStatus + timestamp: datetime + changed_by: str + + +class AlertCreate(BaseModel): + model_config = ConfigDict(extra="forbid") + + transaction_id: UUID + risk_score: float + + @field_validator("risk_score") + @classmethod + def risk_score_in_range(cls, v: float) -> float: + if v < 0.0 or v > 1.0: + raise ValueError("risk_score must be between 0.0 and 1.0 inclusive") + return v + + +class AlertResponse(BaseModel): + id: UUID + transaction_id: UUID + transaction: TransactionResponse + risk_score: float + risk_level: RiskLevel + status: AlertStatus + analyst_id: Optional[str] + contains_pii: bool + created_at: datetime + updated_at: datetime + status_history: list[StatusHistoryEntry] + + +def derive_risk_level(score: float) -> RiskLevel: + if score < 0.3: + return RiskLevel.low + if score < 0.6: + return RiskLevel.medium + if score < 0.8: + return RiskLevel.high + return RiskLevel.critical diff --git a/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc b/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0df9b2e7c764d91939ab4de609a3faebb20c797 GIT binary patch literal 3049 zcmZuzTW=h<6&`YCXJ=c1!uwIN57rJ?WbMwRge$0m|De?}{lx=lfU0+SP zPAGn;>s$_tT=I!*Lq%#N^<4SBpGZ}RgMO~Wfu7$KQ7)qxEndu;Wn3hCd>|6} z@gU`UJUy)TBK(RB-SQ19bBBU&C;Qq_(T>!v;A$lDnBsOOKL_3rH|s7u@~;5O8WTpA z5UD}ipkuIN&^1^!=q*{ScH$kCzP1!OrDKaVmaH>!&pIYYwz8p*_AJ%}L}|9s z&MsNX0mi{tUIRBv& zU3fO&kuL-6z!ra3J0c3wI05&4M*c|tOh2Kpc=96^@Y=FTsd`XP@*Q zNiJkBj}+``H;Rib*}KQ($Eq0iwUC)$f?bK&vOi$})K*r4eyS+m|!%>Z6>rWd;m2u^?$s8aB zYqxbww#gYiojJ3P+_B57ktN<%ZDxq~g#LjTUGD|m>bMHJM7$5WHK63`g>HQe)lyo< z^^EP^*>WWlbhfK!lvu`YV|@8_?<1%&2mX%X(8#@8mfU+n2yB>Gzkx^1jlHpZjFs11 z)72r*S(5KS+Ci@ItYeosg0&s`NFqCsTw4dvZfx`)_U~>6H-q=qKX^ZwN-r6Fbicpe z4-yu<6TJKMFW}k3#VJ+}@3qKmjwGFP>_Yw3yWaamLE+09;|Kj*EfR)YnvZ{0Xz<_h*9&6 zgmtHlz_+`>WvNcu=1l1ZWx{oX%Tn#WU@(}j-EMGMyWQZjcF#?nvqk5MnP0CyeAM3v z-gvhgT>Rgd1nx#RSbOyFVK=ySA-p>I*=tuLcg4x2V>|QOmP%-6Gy<}zApS@o<7qpG z61Pojq+-#4R>>weaIC^c*-+|QA`9?;7Ab+8ud95Y$9UiyCi1qqiXX#WK;UghSN5WG z#7!l4YT}1Dcm_yQvo2Y+rPL!O}C_P+K zbPp8ddjJjEAU<(Oi@4u-jzb!Re&bO3wP!bo1G4EJCA8{+t}`JX`jzw5!tASG`hoZr zm^!TYet*8=BRIK`$;3T?D{O;T2jot}HYqfe1)TBv5gC(HxI&CA!xj|Sf{v+Si|fip zj$w{r0Mx301g;Yon)VBo-Mv!T4qPB?Q`yxu@l*Jw_zA$_%&MuFi!u~%qv*#77$Km2 z44YU*_$2}cs*I3hHYWNwV4RHsc;4f+9j;1u{}!YYZF&@)H|11tKK7iiUw!7+#C=fd zTo;%`;^zpMY-OHRw|U R1JbIs?A9x7(s*-${0}_k54ZpT literal 0 HcmV?d00001 diff --git a/fraud-alert-service/src/routes/alerts.py b/fraud-alert-service/src/routes/alerts.py new file mode 100644 index 00000000..a0a6e7ca --- /dev/null +++ b/fraud-alert-service/src/routes/alerts.py @@ -0,0 +1,102 @@ +import json +import uuid +from datetime import datetime, timezone + +from fastapi import APIRouter, HTTPException + +from src.database import db +from src.models import ( + AlertCreate, + AlertResponse, + AlertStatus, + StatusHistoryEntry, + TransactionResponse, + derive_risk_level, +) + +router = APIRouter(prefix="/alerts", tags=["alerts"]) + + +def _build_alert_response(alert_row, tx_row) -> AlertResponse: + transaction = TransactionResponse( + id=tx_row["id"], + amount=tx_row["amount"], + merchant_name=tx_row["merchant_name"], + merchant_category=tx_row["merchant_category"], + location=tx_row["location"], + timestamp=tx_row["timestamp"], + card_id=tx_row["card_id"], + account_id=tx_row["account_id"], + ) + history = [StatusHistoryEntry(**entry) for entry in json.loads(alert_row["status_history"])] + return AlertResponse( + id=alert_row["id"], + transaction_id=alert_row["transaction_id"], + transaction=transaction, + risk_score=alert_row["risk_score"], + risk_level=alert_row["risk_level"], + status=alert_row["status"], + analyst_id=alert_row["analyst_id"], + contains_pii=bool(alert_row["contains_pii"]), + created_at=alert_row["created_at"], + updated_at=alert_row["updated_at"], + status_history=history, + ) + + +@router.post("", response_model=AlertResponse, status_code=201) +def create_alert(body: AlertCreate): + alert_id = str(uuid.uuid4()) + now = datetime.now(timezone.utc) + risk_level = derive_risk_level(body.risk_score) + initial_history = json.dumps([ + {"status": "pending", "timestamp": now.isoformat(), "changed_by": "system"} + ]) + + with db() as conn: + tx_row = conn.execute( + "SELECT * FROM transactions WHERE id = ?", (str(body.transaction_id),) + ).fetchone() + if tx_row is None: + raise HTTPException(status_code=404, detail="Transaction not found") + + existing = conn.execute( + "SELECT id FROM alerts WHERE transaction_id = ?", (str(body.transaction_id),) + ).fetchone() + if existing is not None: + raise HTTPException(status_code=409, detail="Alert already exists for this transaction") + + conn.execute( + """ + INSERT INTO alerts + (id, transaction_id, risk_score, risk_level, status, analyst_id, + contains_pii, created_at, updated_at, status_history) + VALUES (?, ?, ?, ?, 'pending', NULL, 1, ?, ?, ?) + """, + ( + alert_id, + str(body.transaction_id), + body.risk_score, + risk_level.value, + now.isoformat(), + now.isoformat(), + initial_history, + ), + ) + alert_row = conn.execute("SELECT * FROM alerts WHERE id = ?", (alert_id,)).fetchone() + + return _build_alert_response(alert_row, tx_row) + + +@router.get("/{alert_id}", response_model=AlertResponse) +def get_alert(alert_id: str): + with db() as conn: + alert_row = conn.execute( + "SELECT * FROM alerts WHERE id = ?", (alert_id,) + ).fetchone() + if alert_row is None: + raise HTTPException(status_code=404, detail="Alert not found") + tx_row = conn.execute( + "SELECT * FROM transactions WHERE id = ?", (alert_row["transaction_id"],) + ).fetchone() + return _build_alert_response(alert_row, tx_row) diff --git a/fraud-alert-service/tests/__pycache__/test_alerts.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_alerts.cpython-310-pytest-8.3.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2421b5af765f840dec88e1eb6089a65b2a7942da GIT binary patch literal 11715 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 Date: Fri, 20 Mar 2026 17:30:13 -0400 Subject: [PATCH 06/17] Wrote the state machine specs, updated the todo. --- SPECS/state-machine.md | 73 ++++++++++++++++++++++++++++++++++++++++++ TODO.md | 8 +++++ 2 files changed, 81 insertions(+) create mode 100644 SPECS/state-machine.md diff --git a/SPECS/state-machine.md b/SPECS/state-machine.md new file mode 100644 index 00000000..2b18b6d7 --- /dev/null +++ b/SPECS/state-machine.md @@ -0,0 +1,73 @@ +# Feature Spec: Alert Lifecycle State Machine + +## Goal +- Enforce a strict state machine governing how fraud alerts move through the analyst review pipeline, with full audit trail for every transition. This is the core business logic of the service. + +## Scope +- In: Status transitions via PATCH endpoint, analyst assignment, transition validation, status_history tracking, business rules around assignment and resolution +- Out: Alert creation (see alerts spec), filtering by status (see filtering spec) + +## Requirements + +### Status Transitions +- Valid transitions: + - `pending` → `under_review` (requires analyst_id to be assigned first) + - `under_review` → `confirmed_fraud` + - `under_review` → `false_positive` + - `under_review` → `escalated` +- All other transitions are invalid and must be rejected with 409 Conflict +- Terminal states: `confirmed_fraud`, `false_positive` (no further transitions allowed) +- `escalated` is also terminal for the scope of this service + +### Analyst Assignment +- PATCH /alerts/{id}/assign accepts `analyst_id` (string) +- Assignment is only allowed when status is `pending` or `under_review` +- Cannot assign to an alert in a terminal state (confirmed_fraud, false_positive, escalated) +- Assigning updates `updated_at` +- Re-assignment is allowed (analyst_id can be changed while alert is pending or under_review) + +### Transition Rules +- PATCH /alerts/{id}/status accepts `status` (the target status) and `changed_by` (string identifier) +- Transitioning to `under_review` requires that `analyst_id` is not null (someone must own it) +- `changed_by` is recorded in the status_history entry for the transition +- Each transition appends to `status_history`: `{status: , timestamp: , changed_by: }` +- `updated_at` is refreshed on every transition + +### Audit Trail +- `status_history` is append-only — entries are never modified or deleted +- The full history is returned on GET /alerts/{id} +- History entries are ordered chronologically + +## Acceptance Criteria + +### Valid Transitions +- [ ] pending → under_review succeeds when analyst_id is assigned +- [ ] under_review → confirmed_fraud succeeds +- [ ] under_review → false_positive succeeds +- [ ] under_review → escalated succeeds +- [ ] Each successful transition appends to status_history with correct status, timestamp, and changed_by + +### Invalid Transitions +- [ ] pending → confirmed_fraud returns 409 +- [ ] pending → false_positive returns 409 +- [ ] pending → escalated returns 409 +- [ ] under_review → pending returns 409 +- [ ] confirmed_fraud → any status returns 409 +- [ ] false_positive → any status returns 409 +- [ ] escalated → any status returns 409 +- [ ] pending → under_review without analyst_id assigned returns 409 (or 422) + +### Analyst Assignment +- [ ] Assigning analyst to a pending alert succeeds +- [ ] Assigning analyst to an under_review alert succeeds (re-assignment) +- [ ] Assigning analyst to a confirmed_fraud alert returns 409 +- [ ] Assigning analyst to a false_positive alert returns 409 +- [ ] Assigning analyst to an escalated alert returns 409 +- [ ] Assignment updates the updated_at timestamp + +### Audit Trail +- [ ] A newly created alert has exactly one status_history entry (pending) +- [ ] After transitioning pending → under_review → confirmed_fraud, status_history has 3 entries +- [ ] Status history entries are in chronological order +- [ ] Each entry contains the correct changed_by value from the request +- [ ] Status history is immutable — previous entries are unchanged after new transitions \ No newline at end of file diff --git a/TODO.md b/TODO.md index 972553a5..c951400f 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,11 @@ ## New Feature Proposals - +## In Progress + +### State Machine (SPECS/state-machine.md) +- [ ] Add `AssignRequest` and `StatusUpdateRequest` Pydantic models to `src/models.py` +- [ ] Add `PATCH /alerts/{id}/assign` endpoint to `src/routes/alerts.py` +- [ ] Add `PATCH /alerts/{id}/status` endpoint to `src/routes/alerts.py` with transition validation +- [ ] Write `tests/test_state_machine.py` covering all acceptance criteria + From 35bae25ba5f376dd2872b0f0d8e63792facfba85 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 17:40:06 -0400 Subject: [PATCH 07/17] Implemented state machine functionality based on the spec. Wrote tests, the tests passed successfully. --- TODO.md | 7 - fraud-alert-service/fraud_alerts.db | Bin 65536 -> 155648 bytes .../src/__pycache__/models.cpython-310.pyc | Bin 3705 -> 4250 bytes fraud-alert-service/src/models.py | 19 ++ .../routes/__pycache__/alerts.cpython-310.pyc | Bin 3049 -> 4714 bytes fraud-alert-service/src/routes/alerts.py | 61 +++++ ...state_machine.cpython-310-pytest-8.3.3.pyc | Bin 0 -> 12030 bytes .../tests/test_state_machine.py | 235 ++++++++++++++++++ 8 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 fraud-alert-service/tests/__pycache__/test_state_machine.cpython-310-pytest-8.3.3.pyc create mode 100644 fraud-alert-service/tests/test_state_machine.py diff --git a/TODO.md b/TODO.md index c951400f..4c76e28a 100644 --- a/TODO.md +++ b/TODO.md @@ -6,11 +6,4 @@ ## New Feature Proposals - -## In Progress - -### State Machine (SPECS/state-machine.md) -- [ ] Add `AssignRequest` and `StatusUpdateRequest` Pydantic models to `src/models.py` -- [ ] Add `PATCH /alerts/{id}/assign` endpoint to `src/routes/alerts.py` -- [ ] Add `PATCH /alerts/{id}/status` endpoint to `src/routes/alerts.py` with transition validation -- [ ] Write `tests/test_state_machine.py` covering all acceptance criteria diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db index 1766be290b13358f2cfad9b7b66e68824b1efbee..04b9c5fec590ce0a9466b70b82b599d05e7e7694 100644 GIT binary patch literal 155648 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
q!Y`zwKT5N|^e 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
    Rmv;Z$-IFwPvEGIs8H( z4r!B+sUd7zSQMeONcl>qB;fR|Q}%X=3^LIyAA|Nl#5&UDpIzNn|Xs-a_H)21Lo- znMa&F&TO)3sk^|8XjU>Y9k4k(2Q^iJft)0+)^0 zjlyD02?{%unL$x4t~H^c$S<05A#fLd7C$zcuBo)hqDtPZwa)I{z1+BH$@ zq?aasYKPK9XP-Vl3oQ0oXp>1tVdg_S%ghIpjrTzy4{}A1A{i(N1~<7?63R=khvfB0 z?Tth@h8-8g!7;Zg^`?Ti}!MC*Gugs$Jfhx_vxCX-7o1JS;b*9 zj@h3c*>j~Df%D{~eDrR(lE@L{9#igo*`K67ha$7~{g|p2lxTtDOd~d4UzNoX3S1}i z_#r*b2e|u!9%(}nJm%J-d9e=lFTbQ)GfSKHdeskS?NXv-_e&}zY<}&r@2S)zztjIL zuBoHvOYVAP?{+o97poDz7gAj=?YYAyX3hx5Tw2E5#t<*3I84YYf>BuKzM($CWwP%` zvFFgiBNQ{guc*Fo)m6p~soQIqVUUm4JDJDm1{;IKUo_&oUvk{RUZE_#(XJ__ z=d12|__fciou22Au}a+{2tEreM7*3369UR#v$P=Ghk@CrMwZTPS$(wiPfzK;mzRUn z6|Q?-*Iv@Cnn4Y9P15d{)Q0-54*vgD#@ylAKc9Wk%%^6yO}~5k+^IKCjZDtBzuA7t z#HS}NZ@ss*Y5Yy&qht4t{>A9+&CfKi7F*cS@?Ltc>ql%tj{L= zs+m;8$_N>6v^3i*X)>3L0x9B2p+&I+s1-s82y+hyl>aMSDtBzOG*?v8aBuHY27zmP z>Mu$nI=V?nGNi&g1mPuY*@3sw(mc14hWxdh(pprj2qlC@=!nP|tkuLL&{&y85PEO4 zG?!P>Q0F9YbE*2I{6px@K&}Ax2`pkGl$rv!vEkA@r;>(BI^0ZAq)m81@m5LtN=cK3 z?uF_NR7DKIO>DGowpG&TkeP3^G;t*jiYaj-Nh7Do52><(NGiF!g9f@J!dOxvFy3fs zqDmUll)?7E)dU5*j1)hj0+RF~W3f{{5?MDqPbaLTiTf84+3;B$RMNnA9TE_yRwI1$ zAjaSfA&9daxW^!|w7pVi{gep?eMoZ&W z(xh%eRY=YQ&H+vaU?K$NIIKX7P;CKZ00ovdS{k>KCW9m|r3N@F2~sJFSW#BM;f!!| zYN$gxo)+$g&l{(bMyF!XM(f6|q|qr3ywTEFl{5e$sPqf~HDs+5qof#ELi#^7BM2T* z?T4Zn8@{HVT}eaPH@qKe&NvQdrEum;$!ew%RYpQ;t3)83Hd;5ARnmkSg(5au8gp41 zjXDb(EzPBsG&+}&8!gQxl{7l{fEz8%vnpwHZpAiQnu{xGbZ(|LTAHnuG&;9K8!Cj!pvF2NGQAkO?taNY$GWDIo^N8TZ>WzoWfiHrg!$~pLf`~$qYaMIw!Atia ztFC)bM!U0G);`v?mvk#SGwzo0(WE*0U}MTyqQb&lsy zITz$%NWClSd|7075)0C?zt=j}nD9N
    !=#hr2OV_kbmw{iwG)-I^a#asODmke~Q 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 delta 1424 zcmZWpT}%~66yDi8yL)$LcZVw0wo$>j%e{*%irSYdRBKd{8Vc9)AU_sc#WX4i(OS4z zAc{{FsHe$@sY(>nw7Gy6Qo)B-(AcH|nwIeMqIk7!Vj^DOoj8>g#{Hd0E*jAv#A?Xa@U)5Z`btk}Fxnhc3i z>ptlhSCw*Q5e^t_@@2%eLOdW|at)tn z$BmFLL)0Quo+nMCGIhSs?>mYX;VNH*l*(&;vj*)@cl+~ju9+>LfX|b&Fxn)!y#EMt zVJ0~QU11y64=GT&)P<6;jdnxd(&upZl8wqCRvkoTkZ{+9_6Zl>yy-#z9S0)IJa}-+ zfmv~uJ-3zWxu*Z~+(uQf_l^e}s%=yW1GbBmQ&Ny`=s-3!{+te*h8!q&T&y-q0r<*U z1~FkYZl}V+l*3p%CIw-8qYdinUS61ypE5B#?^z)lwcW=@dk>0T;Wzd>L*{ z+i+_@h5Vp{^#jrnkL{2h?0+hvz7FQZO)lSBgItco$bl`ci}gw2JLn$9q)C8BZY!om z0{pPmhMv@m*pP%$vb+@=VJXCO3k91%E_i7<*ccL^Rsgd?3c)XFt=OCr;H`zWl3+Cp z-&rpQTYCg}>>b9eSZag4=}sJk;gxX*uB5lcx50&-D;EB5QCxU;FjyPkfsM*WN!iH8 zl!xjr*1;NBk-CM3QSN4CozbgXdTy%7&y@3EC#bF|U( zw(`*&8tk>vPQEkFaJxWuPo{bNLZO_)%Zg-Ij40+eie!gp6$|Y}z>7}`@7^JA<10T9 zmc|qh4taA^-U8#MkJ620^OYa7q{6s7WLrC6Qj&w?yy~qqVCSl$L1y5v9B3_r1jTs;?ik$6mDlgs;1Q`fcN1 DOFY22 diff --git a/fraud-alert-service/src/__pycache__/models.cpython-310.pyc b/fraud-alert-service/src/__pycache__/models.cpython-310.pyc index 1f324ab0129a0e20d2fa00c2a0d469b899577714..eb02d481130de14f508a8d6092c8d034f3235677 100644 GIT binary patch delta 569 zcmZ{gJxjw-6ozwc8e?l>tF-A?Y#l^{RTl?8u|bPgG}Rew`6(uIXx_NqTy~G9K9nJ40fvJFk z8FE{Gz4VhuDNq`uH6vaI_ADsZBuLP&ZmY-3uL(3wwQGTDzI+w&3g9y|3qITRI?egR zH18)4L|UNwwdfV;up=1751hdu9ho*lKWrWQP>a=*hY9i!=PU6XDe*?6xJE@d9PKD( z5VQ_YZ2!FN2O-PDyR3j%2UtcgOQA_4G(<0=jF?7D@lszVRfqBz6yTL(LX7kiR_B$t zPR1c?%%)Muk~%|9*a+Lf?io(@H*pz1SOHju{D15$o|@wMK3&_!5){Y)jFmBXo4@tn zksaPvMvE8NRRJ=Qp!*|-l|#V=Dy>a?yggH*c>+J zyUjgwv$neL+NR~y+*;jo7~Y+ACz-^`h99Ew4Rrvn?y0((P-BrnB7P;LL^LgZ1A`uN Al>h($ delta 152 zcmbQG_)|tFpO=@50SMT4?#(>R$-wX!#6bqEK#l_t7e8T`sB=LgMJ!b`MLbm?jU`1Q zMY4q@N-RYxm_bu|a|+`r?#Zk8rZM_YcH~!;PX%e=0}>pJY>Y6>##j^p6!Oy)pWMn{ c!lnyk7X?f%;1r)6FTl?zGdW*CmPw2c0LJ4Sq5uE@ diff --git a/fraud-alert-service/src/models.py b/fraud-alert-service/src/models.py index ea642170..58b58116 100644 --- a/fraud-alert-service/src/models.py +++ b/fraud-alert-service/src/models.py @@ -98,6 +98,25 @@ class AlertResponse(BaseModel): status_history: list[StatusHistoryEntry] +TERMINAL_STATUSES = {AlertStatus.confirmed_fraud, AlertStatus.false_positive, AlertStatus.escalated} + +VALID_TRANSITIONS = { + AlertStatus.pending: {AlertStatus.under_review}, + AlertStatus.under_review: {AlertStatus.confirmed_fraud, AlertStatus.false_positive, AlertStatus.escalated}, +} + + +class AssignRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + analyst_id: str + + +class StatusUpdateRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + status: AlertStatus + changed_by: str + + def derive_risk_level(score: float) -> RiskLevel: if score < 0.3: return RiskLevel.low diff --git a/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc b/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc index e0df9b2e7c764d91939ab4de609a3faebb20c797..0c684e75a6ffea0d2846aca365e667382ff64e9d 100644 GIT binary patch delta 2477 zcmbVN&2Jk;6yMoh+v~5jlQ@a(IPKDgg4@zG;;WyuZewU@n}#?g!U9<<&n9(<*I{;? z(BR0Tsd|f0x;F}veS~WyP9PyJ{0|l<4oIyya7NJb-mKH6O^O7z_NO=V-n{wx{odRk zd@!QdqS259zwf^Pp>is@sb^$A**yATvl=hPt3Ab@YHzW(+E?r&lI5d*8ki?k+mMS1 z8nnW5G7Zu2ZME2MB^Cx~#0t|W)i)FxvqIpD)1KSvIw=l`FyuavFC@cYX6252?0Cm#*__>E8FXgcape{{ z=>DvHdqLzn0KJ_sXQu6#WnQV->>$RCAiRih2;n7!ml3iEF92kHEaq;hW2wW?=mc(5 z9A0}o3kL0X|5no^=_Y+A$=KGk&m_@r6fY+(w&VWjFOb*WMBp^Jzcm&3oP^^zOiX{o z{Y4ug2V4?NH^-ni8)R?b7|mR?7%x>Q8^>5&WieK(~-u2#1xJBzN<2zb(PXnwRWAxt3n5X2T$b{Xv} z2x7IkgJ+iX+^YY5`h1%1y!b2WR>wDC&36ERHNT)gRU zgVzkx;1;V^pu-S%yL$He)Oc z9akM?c=8@m6}0jQdtHAJ9Z>`!ws+7Hln8i~Kcr$ky=Plc?5cLiauCYqiTg zXmM>J`mxL=fmgv4a0v)JDTai)1lC(I@P$(nkU!7}wBne0p$FL73v49|eKhhF2&2o` zsORN+E71tFNjFNGQ$0>McWDMmt-}Pd9i-NFk^1gar0xe&Yl75qBsE<7b2*)uu$Rq6 zV4bMll_u8iQ$`)@iSg^5yN2M>>YH8WS8K}_@*PT&#Y)Utu2?I^N`>F3)j<|^R~Z)F zCc7x{>?9;xy}DZw-sO90@a$5vTYdjUBD)ka@ZS`o!)iJT6u^$+SX{g$#mM%Cnz$o& z1dr?}#`(>qB@62ByVxTXbqXyk@Se7U&(#)t4-*8;#l_l+MHvc4ye7}q;;2^$JOxfW zcOcHeN%m5g4tj#e5S|uY&Ip|)pVis@P@2uc@a#Ts8yDO|;g6bLUBIog7(yJN<7aSf z784uks7p16Gkl>~0U?Jlg<-xWlb3G@@ruklHXvTY{TQU3sn%$H(RvH?Dmey#CTS#2 a;&MbjP7-=lSM;Gci5!71njn#6lKcbn?=q19 delta 777 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 Date: Fri, 20 Mar 2026 17:44:31 -0400 Subject: [PATCH 08/17] Updated the todo list to reflect what will be done regarding pii masking. We will revisit GET /alerts masking after we finish the filtering functionality in the future. --- TODO.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TODO.md b/TODO.md index 4c76e28a..beb7b7a6 100644 --- a/TODO.md +++ b/TODO.md @@ -6,4 +6,13 @@ ## New Feature Proposals - +## In Progress + +### PII Masking (SPECS/pii-masking.md) +- [ ] Add `mask_pii` utility function to `src/pii.py` +- [ ] Apply masking to `POST /transactions` and `GET /transactions/{id}` responses (with `show_pii` query param) +- [ ] Apply masking to `GET /alerts/{id}` embedded transaction (with `show_pii` query param) +- [ ] Write `tests/test_pii_masking.py` covering all acceptance criteria +- NOTE: `GET /alerts` (list) masking deferred until filtering spec is implemented + From 235a9057d77cff7b4b2349f8e0bda7b4b45711fb Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 17:56:27 -0400 Subject: [PATCH 09/17] Implemented PII masking, wrote tests, and ensured that the test passed after some parameter issues. --- TODO.md | 8 - fraud-alert-service/fraud_alerts.db | Bin 155648 -> 290816 bytes .../src/__pycache__/pii.cpython-310.pyc | Bin 0 -> 655 bytes fraud-alert-service/src/pii.py | 14 ++ .../routes/__pycache__/alerts.cpython-310.pyc | Bin 4714 -> 4940 bytes .../__pycache__/transactions.cpython-310.pyc | Bin 1941 -> 2160 bytes fraud-alert-service/src/routes/alerts.py | 13 +- .../src/routes/transactions.py | 16 +- ...t_pii_masking.cpython-310-pytest-8.3.3.pyc | Bin 0 -> 9582 bytes ..._transactions.cpython-310-pytest-8.3.3.pyc | Bin 7453 -> 7792 bytes fraud-alert-service/tests/test_pii_masking.py | 169 ++++++++++++++++++ .../tests/test_transactions.py | 13 +- 12 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 fraud-alert-service/src/__pycache__/pii.cpython-310.pyc create mode 100644 fraud-alert-service/src/pii.py create mode 100644 fraud-alert-service/tests/__pycache__/test_pii_masking.cpython-310-pytest-8.3.3.pyc create mode 100644 fraud-alert-service/tests/test_pii_masking.py diff --git a/TODO.md b/TODO.md index beb7b7a6..e0b69629 100644 --- a/TODO.md +++ b/TODO.md @@ -6,13 +6,5 @@ ## New Feature Proposals - -## In Progress - -### PII Masking (SPECS/pii-masking.md) -- [ ] Add `mask_pii` utility function to `src/pii.py` -- [ ] Apply masking to `POST /transactions` and `GET /transactions/{id}` responses (with `show_pii` query param) -- [ ] Apply masking to `GET /alerts/{id}` embedded transaction (with `show_pii` query param) -- [ ] Write `tests/test_pii_masking.py` covering all acceptance criteria -- NOTE: `GET /alerts` (list) masking deferred until filtering spec is implemented diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db index 04b9c5fec590ce0a9466b70b82b599d05e7e7694..38ddfded0e39b6397476033857654ec8f02f7d55 100644 GIT binary patch delta 78933 zcmc${2e>3vwf9}S^XZcqBug6R%rIx5XDW1cb%A69A~_D3N{!^Kh{EBV!K+?G8H!7p z6Nmv(0RhE}2qGvTf+Ai;1g@x~G;s>xSuF!@fN7_XW9$K;71Q@ zW2;weP@DK2zl>#*-<{lI;&+o*Pwq8&?hTu4{mF@7@@nI#>GC0^t`qon*N<9puKLeOUb2Z4_u5V`?DWi**>&7jJMIQ8-=U7>dUj-4J*Vr0 z>OGX6NbjARZtx$jRLh#9YN}>r{N6r&%c_>zJ$>)P(-nS>r{~{0eZ5)Py0Q6W$ZzA- z>Dd08Z2iIkn*5K+3n$N;JbUug$=6SYYiGB=;q?7~KXDv2UNz_O_my)i`McZPGXCx~ zw>f`zoRecFv)l4_ zYIc+Kj$?P9C{IdlW0%SAOn#gJT>rSUwsmk&|dPJdEUyGhB-<(u7Z zWOw|q(+YyvYPEZQw-vOVPRsFny-w&vj@k8%v_$dpCeDaMqtbckkc^to2 z%3Gqy3nR--T0tk_EwO30LeukFkr^ePADTfsh>W`^{c*L1M;@8HX!6MF?X?q{)$$*zo0hJuZCLsGq&?ZF?$LN`;<4tM@;UWI z6F1erG;ztu(G$_c4ik&Ue?ER|<^AKAm;R#uQ8}Y|?f8k~yVZ6X-=uM4eaFVdYI^K< zwV$g8cOSc}vPJXok?Y1jII70ZYOYe}SARA3y2kj}HX}}Hzw$TLCn~?Izq|CU(Z7{H zJo-T8HKSKoGoB>1TN~c!cGaxBc)0LBRKJ&!mY;N!p6j>lUdW0!y^ta8 zgsraY_`IVVcalimK0PDNyZkV_cB|t%F)!tPVJD1Rv1dk65O$-$_0`|?OM5{#jxAaa zdfisrilSDxV@55v+p{`R&oz@?Tv|^`+P3cnvF)~!c1LE^?6pEK^yx_4jU3lYT&t~q zHeF7i`s$~bnU)*IiD$NA*QIbMuWfg|R;QiVy~y&dAP&_B(l6e=dd6~ki5Yr+D@wZb zz;4H_u;<1t+iNF&*YEXA$1iQ6hsq4Y#0qT2)8wU2+iZnN64FM}@qFKl13N4&(j_e` zPTH}Ha6lxqoy2d2ysy;>Oy9M8U5n9B59^XMiDSDRkI!;k-W>WJfy`Fks-iaVKyFI66 zhn+TK@0%^Z?M5vx3i*mCFe4{XcSy->`@Kt3>1V%Y?r#RMV_6o1o5*}we5c=Im;=WP zd#3FRPuYYASv9PIJZ#e6#C%%y=wy99EUz>%=TkmbvR$j_0~+FR5dBaqPD_6Iet6-IoPo zGWT{8nT{Q>rX5$Um6A-DZ^dj)$K!SFDDc^zHbdgJ!;bBA=}%8B(IuxlX4ec8x7CZf zjET)b6C&GMVHn4~ZYK=8Oy+0miLoNv{+(*-P~#rml^_Y@j?b1$LN=GfK^DlLPHc5T zKMC7*9I4Og1`?;|+sr5(qNLZh_%9Y`QN9=@2m;KXW zcG%0Uz>kxb8+cK-%X&mQlnmW~=^_=8Z*9JePP09`?bvU{Rvbqh6%K+$SyIySeS{tT zwQK}IyBBs_et)1zS@EGyU-@jlFJh+$wlC7A8^vD7YqN7&y7?$V zQU+ct>9{hFCW|BJSS`==13Kz;yj25F6y;g3@Ibgwq#ud?N-bX_S)@E6j5?J zE$3}BMo4j%1_`f1xcMk8uE#!Rogis?VQeeC@-12T0qPA)k#P#8k6otBk2^uggtIz5 zwe9G3jA$o9lqEsS>Lf^{9wH_*BZNWZ*j~p$hB!TSzn1e>Y&urNXopUlp4wr;I-t)^ z99ogZ7g1ZCqrb_cgpJ5R%c7$_hgIe?!YBZ?>oET89CfxX*-c!wFryuJ8T>dw%=ztZ zD`IofF9fn{t23s{Gt5&GbbQm{AmAAT<-7bB`=g8WW3$oy*i~1^ORW&mlUQMk!;g|N zJ1KO!Xa|;y!18*s!_`-H^KsI%6HBCt$AnrOGXV-wo2^(r-Ml zEWPNlseHkQ9WykMR*v6ge0*8@J?6E;`4!ncPK?Md>HTK=km$ZI{EZ8$^*?+Ez-oLmKmCzxlk5 zMU1W{8`(w>BHSFGg8SJY4gPC(HoEb zogf2Qo4hYHZ8oawTdv#VJWZm~M!KV&utz_82qKnwJ7Dbvh#w@FXS;5~Ipvt8mr-)t zQaiBFa~PhaiynqFLfZH-rvMs77cs^NIHmQoXWy@ur~BSjTVV%&XxqHi4bdptHpc`K zo=u8~;(X|l5E=QA^Ou_=b&gDb4P7|c00-gBZZnN zGZBaEedKB!cUaqqY!<27Me0z}aZMX35~9?~`sFhbd;w=z?1z5rS{-$7I(Em3^!9H| zuh3O6pBx)~$Mj52zli?%Hd7fo92HSwdaOCsO;>z<)im{ya`Zp3IjA_tD9JJJTAY$j z7zUQ#R#)k+?|KDY-}>%adfDyE(xcwIe3jPX+TD;opmMvshwtEHx}C^sxluRh*;e4W zp{X9wqwwR;Po+D4bA_Jrwi`1&HmU+j6#D@A=sWE180(w=4;WZWJtpm@Tc5#Vxg-7I z*H&ehU8a_#7v5Q0(dI)iG-AfyL_PMp?Ds@oWV?14p;yx>b+T^t)W1)qmwdCHUV6uL zntpR>MSsiw7s?8JEIn=Mbo%2vR_@Y=sT zanJKm%)>72wS!J8L`blodtqp^?hr)kC+WdoUy;v**=b{Rq0pH=9qq}kaC=Fs$HM9M z5|&DssFN})Q`4*dDeuQXNt|{f6Be`UIWAgQ5lBDJbATYgA39NKOn<~)`;?}(SK8`! z&}%)16_~j61>*;)#tV=~HU^jLw5^_}-jP1>%~eYi4;_ATsGN@{IN2b4s(?*<%P)PT7v{hK_n%cN|*}C8Ea=Myxrn%>fCH z((>FcXKoZ?NVJvS1P-!0wAp^4k8Bqyi*gjUkt{(MQy}0)Jrz!u8I5fGVwtG>Qum&G zW%IA>D^^;9YQ}h?DhFZYfD>Tw2OYGn&{7{t|M~TmGfvQP4?Rr^mH<3CIuO16jj++9<#&NHCgj< z-0j(@F{QGsJ133q+2>&WcGpF##xgLwA%ankoxlm57M6?SduE627prITl4j&tNawao zKigfCE?}0UB)2&Q9nKvmP*0^_T|BK#_^(y_7gu|Jqy?EIwXBc#mGWg7uCL?u0lY+1BO6SI>84IPZ)K5Gnd zg81@1AC1^l@7Jfi9VZfWO6cEq?cO^34p`N!5l;A1Nwot+-S&Q`zDB|{A3kikVW z;GLN2==7ix0;nX28cus#eS7>$_4uVT>Qlyk#`g`iUE|rtL)Bdy*OZ@b%r*{dv>KKA z59^<=|403(x>Fyi+);b1c4O_G)s1Rl&96;Xe^$MCwA( zAN|1SnWKA-ZrS`}^Pc8Mnr~?C)0`Uld*ziQ4~$%0`Sr+KMh>iOHnO~YSNQ|wGs}CH zw=Dg!bZ_Znr8kxKD=kz1RNqyfN+0~{ru%m=U!x9Y4yG$&u#Hjab40Mb7|0%GB)g*2 zl)>p(5mq}UEt(oDFAQ0E9s&tlEW$Q(QMQ6oBmKuiD`z^K+$d(q)($6jn|Wz5=|M~H zKQU~3maX0-FLiqu;2l(pkOf8aNS&^oh`DN7NhfiU8xA9FlwQ-dsV2+K4#hx0M`Qv! zX!EY^v5(qd#DQ8avZ0%PaOsqmCtZuaSy+^qXRJPiJ4*`5f`W?S*^>?5Q%9t~dibFO z(G$R3S`N^UEGA%;4lVFbv)ze-AKH<6PCATkhWKEA1}GxvqFodVK&zJR(wWd=&=OPq zQc7muzMK>NiSKW(yMl}cai!-1GO(qW%?b!kHG9W9Y;NzUb>RKs0YR|nHfRE zFee#8wnTvP+HyNsewbMS+E=M9CFS69=silONl97RXpf*37-|W)h-W*Uk~XkC0Nh9v z0{=GO0gA5~S<(k~Hj6{x{=`KCqswhaogl-5 z@r{s0^J2)*yinKBywEZ{v|x_W5T({tb5b(f=W#W)z3~tEZ-xUG0^7x61myGu{{u)v zY+7E|k%vI8-%R&;Bp2EqP?rzd#s~^-guMsIft7@x%@f%_usY{oq&n?+Lz3P$a z8P5bGPjF{gOg}>s0&+yC{+wX!&(|#k1F5qNZjHjsG!QF4@#&!TY z7y<_(^_gmRS{r%H;PU|%^1stof72x~Q?M~mvMs^KfSWlf9KVIR9#}z?V8^-YYuQ)P zl+suI=t6C=bbvoGE^P5@&}AEW9hh{vXL)YiGusLKK>bN?M|1C~^fy1Umb>g2ydn5D zr~onw5KW)Z&P>*fjlyoL-K9I(V-F*UcYXBWTwWoB!x$+QqX{sI=tF)0X6y%GqXJ2w zW2(#3%@?zqEHm94$$ipL_8JQb7d}P%~wed*f`o`NE$28iFX8rN{&Gk#_QQfak)_%5o?bceV z9<7~F+oiT)^;gx~t6B9FbwlZM)jg`4s&^PauRLA3yYivRIhDOD+m!!SexOt@f1-SW za?1yow=0)QKTwO-&Mwu0y^txkF~HH(LPxUfl9JmNF zqqdWGIv_KcEH4htsO7r(0?3TgXp>pMiKUk5uNT+c%mThSrB6w-fDb`!qra(uEvpvG zmO*cGTsOVvj3w!McT8nJzD(89yS}kxyPE#q0%(Tu@$3~>s8;rq%hYsw+?%oLKR>nI zVqLok(lC;4e#T0H7pBv9-?1d!@9b)N*3zl!sPrxSsHLX1)3kwh0c+m4N38k2^<2c* zuIr&HVrMsX$pUt^@rCJwWK9(?w2j;KOADCQ#^adP%MUd!%PTBwTpJo=XI>XJe>X_C zIH#2cGfTAubP5=<#^1z{U9nns##-1oHSW(#E^M3{ztyuoy94Li-cqTE*=c-Qmn>jR z8jtCcMNGa4L04B6LRO7W=n0bi5x_w)*UoYcxeX z*v1d^x>~deBkAgX74U-@f689{U#gW}xa(B4A@$SSzcr;+rq{l)mA>vx`5p&TT-aBv z(s!Si&qxslfN{5Uum}OcxHD_8K+>_VO|5#3u2jH9Z)AEXgeq|OF}hL#XT1SQMJO^0 zySt6+^+@e}`&9PW6{?yEkzpp%wTtk38oFc=e$R$^$%UPi#?$HT=j7`a!+k+_y78eV zTG>%*!l)cj*|z*I^?3QA@-^kT@@myBuTnRatEERu*O!i&{Jxr){OsggCy#_jH8Szo z#7z_L1W@Xh+7lDwKOO(__}cN~$9G0Y{iS;6*zIGd2&(j|v6qiNGkW*vheppH-D`Al z^RLbOOK)#p)jY3xKy$j(9C;2k_1cj&T1~Arex%jZV;gQ`tp30CFV^2xkLx?s7uBAs z-B!Cog|(Awd(<|qK3%=L`oZd1)xD})SN>A@R^`gdd1`#^?ABuV z|CO3rb&{Tk0-&Dp6DgU^ZdL+he@FWG8*-4n2xek@63nCp4lOoG@i_MIae*uF8Z8Wku5)4EiV}tXV;ynmaFmXhi9tk>KZLO@B89Z`rMnV z>E#zpr)Qoqos}+ExlIY{u!p}DJWXgL0O(-rUGYtKg*OQY4HBNAxg%ZN)wuhCP%QkV zZ2%2?fIuJ}d>=Txk}gK4@ki-5P6K!dAp4#p1VLb6+)Kb{06$Tb*d5nwo5lzAY6isz zO2tP_W5^OHCjv}^ueLpCj=0N#M2&~_OYz(R-k36L42BCz3Qj_Ax+ZR37)1C*jL+$h zUHtIWcAC+^hk%-i$0ra!Aq+!?8pi-^LI+9Zn*e#nf2F5CynJSnZUfW{@DX}!B&>=k z0tUfv37P>Q1FX^M#2w>1x}*){0o4x=25vMut9fI1_&ZP?B2^!{T}D^TouX-3gOzDZd*QNB;9`b zR2rX|pLh_;TvsrBd|={1lJy^ZF%DK!97r7)DT(n>-9Q*I+_nv852yxa5b&NWG%3(x zHwMzheQ4aOO9nA)Gz*m21X_l9DPAdYdSg;|#3u^N&v<#6=LZbVC~JA`kFCa%6QJ5*^DB?3qsE*LzvzQg;) zcgh!o_F9pTT!Hk88n}CY{OV#1j)=teDk@Dz%As*Y8uWD7Pni6FV7C zRiCY0U3$EJZsmxH4eI-rj~V~f`0eV+%0+5@)f<0L_0ZZ|YRemYjh|YY7=P9H=3{>t zyQg~L*hk0CRkw`oH@2)=8vWPkL$4gYru5FyxzW|5=4ibXHXkiFn_p;N+zgstb7JJD zBmZ6g(MURS;>d2b2Szq&{I2rZ#$Bau?ZCzdtDi2vU0vEZySjblf7Z?}(|U}Beh`cM z*~bgPrVzRpj01dlF1v(6fJI(v}5usQh{zH^FynR0Cv4ZY} zYKRA`CnrfK7Tz@6b3uJ^@FhtQ;xxxs6)GL=;NaqfLxzAi5#1U)4}|cB`y4Vs$9C~H zioLEmlav*YX{RSbtb;qj2Zk4-Jf{Xdg}acW{&vE5#Q14o>e9RkIn5VB8!Q)2XBbE! zqW&7`C>+-i&pfmzR6E{@Yrq%o5FB;@Yn&o*7u6IcXW+7U7VKpl8JsKhR1V-CenJno zIMUGx-F9qzLS70V0_R=?BMZihG>>~C#-|3+56>0kd>S=AE+rwS>Jdh4iR_7b5|aRX z2%7JTR&ZZIKHLs_%& z`wDLw!Y__BC`gXW353H3*GQmD{Yjk}Fbo^|2L~Ag6$=l;MuAH#yq>s@CB`YolP#x9 z>;Qf2Y6T)j=?HCcQ+9g3Wd-PFcF(v&f0HG84Q_Z3FD=ViT#LTY>LOGX5BE=Gy7^HD z@oNSa7Oq~1{o*dcDG7HSG9wGL3n>hOZrk{^erXSalFw4+6Zm=zEXGOLL-^w82geEe zh-N?FV?pDD8-;6%6AlNG5VXWcY{Asw&0SPQZE(BYZa8DNnNgyKXdZqe?_>F6t-#gg zNEWS9!(WaL?h;K%2-9t}C7FBLfKtT*fdd^o#_y%0`0zSVxtMHEsBRsH!DpV>hY*{6 z3%!67Ge`FTZ`w0_RSU;C1C^i-Kn_BUMD>b+4WM)c#*gKt_@z9&YHU2VA79T(4aMPT zcdd4Ux*5X`H6GQ?JMv3rd$2<|s4#Hfned7RyQGm+W#rw~8g4%@^D-;(>CJr91&^3M_FNK7G`&P&YtbaZ! z!eu9s4}2Sn5z#QX-Bnq4)OH+}*bKIFD10ghpDkmH*a>3%`VeKjz|Wm7Oa|>E71_GX zy^!zV-m|J;5D--p1%&41$1sL-WX503+dUKR? zl<;;roFEDi1=O-;+TwO(z41+$127XEf@a`XCOxJ7DrOjYfT_?OB&dh!mJPYi;)a~^gR&ro%C1ejsu2_q> zy-n!KEa@&KBjswfJvPl*-6aycV(BzzyHr|YI)B4e0NtK zuT9rPt@M(QO{vY(JE->UFVNJn$4_Uky$)r!94}9A38%BQF)bXMoq8SS#LO%C@4eF} zPRyBi*|SS9N*=pK)zf`Wnab|{BH-rbr%q?P-b8B$%iG?5{B-(cZ#q4Ouet58Db>i5 zn|ROp;PcsS*Qsi{$%)h1V)?}1ov5z#ZO*{Z>A5(AFo3$ruH`m@(OKLmT*027e`qk1*^@{4rxOO+I zJX86G`cHM7>Zoz!$+fdnrSg;6@4lt>)yh{8nK0kcR5)8jUK7N|A(`e=xSz9 zX~jK&bc8K~az?y~SjeI&!1-dwLamJ59=3KK)CB>M^A7|jh!0Ccz3v)@1b0PAgcUWCca*jHpx`I6T2Ye`oU%|qL=0r)NRlEB5CDewqnDZ>@ z$LXNIK?=gi$5Y0^@6oPLvtK4*=9S0IAY94giZq6F+cmY&lWG?Ow1SJAb zI?92T^a$Q+b13umo9T|ib%OOAwR#d{(H5#?$k)L}3t><|QihUad`b2wtlAjykzMhu z+9KQU3jo<4xUrAIK-m&{2SOi@hM#r#UDBcj56rC zl$3xpXnG6|WKooB34%6mSY9rP8+m(tC_C#~U^-|pV))BpVxo6+g`$cUhg&d#Uc<5T zwd|;GtE&!{^5`QjAOq(&Dm=phNdWRc7yxWkMw{VIq8vB$+w@qX=~$4mR-~lKdgUa- zf^gBmlF;~8`u)?UbAxqe|0y+@9`O1c67Wq}hwMw#6j|eh8nD`lN8-GRJPyBzWsiUF zT|HboPt2oF#L`8a%vcH%ZO0i0v_@?dM~MPNH}6`;XVRTd&0!T(Ul;5EeiidWarBL@ z%aUVW0W8>r{C^*bBghuU6H6TOpv)|rnBB^0f|(P56WT;3Xs-v{C-{{tbK)Ht&Oo3V z3@o%nbP{MqOpRmwe0lo0la_0-1ggj>&WbmCa_%9D=rR)x6^fPEhHGUzd0-`U8`KLF zQsfsOmuMAU0!||NU)bf??;1DD6rqbCq|ik>@N;CvqMQjB+l9g@F??OK?HhmA>#z%d zi1B1LJ;C#_V_F0;5XJ|ASz>FDaGr6cerd;L6(Z}rs>+^LT!scMsGSKKi5Z{#2NA&w~kEvtQ<^3haKLSfL z!Az|fK@vq|iv5BWhbUpoIgVN{wW8e32a4Jz*4h?AJ;fdN6BrYV7+a3*iG@GHb# z>^OQOrV|4miTT3J_drkKXyc_sAABzT^T~ZU4`Z1@#|T0DqaU0durNhIj)`;(Lnm$r zh7Nj$AxH=iGYQw90TeiocvLE4v)L}dH*sQy4h8YBm$5rgt%PdK#>2W63(H3)5TWi7 zJsHEqkQjOFr5=JCy&B0X`-!5WL8F$2lx75-ppPbEC%`Q>v6(qR*n()h4ibf90)!Cr z6eC29%A_E#h}2=y7(x~&p`e_Za)fqRE@*)GsUqV&)8*w9^`c&cm=(e#Gwq^n5?2=i zE`YHI*sSpXaN52J%_TeaOJMAuxk2^E4Ynb^zXw?b z9X=NxrbK37=FvVjM<7NeIwKee0*SLs2d>aTKYG>FcKQS&h6DO5eP*39-((Ab!pX;AN%{bgCR@UUp>lhYi!!)@0V1A26)*;h zOT0qNCc+yPqJ$Gqk=VwA*?aidoZSbUY5Jmo%O*$8urvfGg5pU)5o?Ac+4zTE;ZHxp z3O_zu|9X_ZuLo1PV)>B{u{b$3rXx||w!on%5quuXFO!EsnFwf(I}aP)_(}Frl*Y`x zSuN8d9)y=Ju6cDKud&b}O%LVBN;Aj!U zDkb@Y?EA z+}L;R?8?=1_1R6v4mqU0;meJYefF8%r^qIV1?6MsBAe-8oBw9>fxV+g;?;%}%4$d# z9lm(9-PysmRuV&K`Pp?#)rQY|@ZeIl!^ogScHDQ=vO{M#9_|Z}+JfS`NG^cq^@TW? zY|*NrzUT+5)ed9xec^$X-hR}QLuWU7UT+GVv_<@4!@V(G(@E8pgMrb%7hk!<{6NX? zY`52{#fKlwL~S_S9i-QS>!pY%Yp6SdV>sC};b3s`2a6&#-=X}0I;h{F4Td{}f}|+A@wCow%w(* zEhj2;f8;cqg7YcFNCQp<*&+mkBia-NCcW-$(W*IXP^SpbB(Gx*>J)Lr=5J;Hj z<#kpL>iDo(i5dW&Az)l|ee8!00);pN!Yw>dv#$Q0m2<5@9ZSOfSw}CDF(NzTA{DQi z+kQ}otT_wjj>!AA-MTsjvPejsm2)cwb;vlgP}F0(=oY#*J*dKzdcb_>V+p)=$?<@> zjQZ^2qVO<&&`!E}Gs_2c3WPO7(n$V0KF`jz@SiAyj`Z>Zl?f4tF@_=H+-;tPZm2DM8kyvna@Pfm=h`^TSb z98=k>{B+I)tF1r2bNK_6J1XxHMp)(4+AVOxvawUE8iw)f#Y60d0+BnWLKOpr8sh~U!gLx#Y zc)7M4dXnf2VJWT|w5$&C#sn`ph*UISOd8y8K}qY!33*1kz~x99%s`B36i94GqVo{K zIOxPtSJLiFU=c!9u=p_Jkjs(e>vEz0&;oJ47}9>=m9Q}IAA)%Suu020gOZ@$bVm$l zpRhgRApImMjmS6Qx*=i^y8_%un#W5*5DwZ_uWR-&T?BVmf7B(hcd$@Er2`+MwhNk1 zNRoI_gorG00I{W>rsPbi2jLE#k<40HXnei+M8#h1!q5R7L*7QQ`mKH`=`U;)S3C|V zmYDRSIfJNR9FipfnA;-|>Nipnms$_k7lHxQK%gh^3ZkPWN|OLLf+N8mmG(=`@J+;W z@lx~>z6o228HJ1o++>m0iWEzhsh-d;1-Xh79G!RpuxKJ&dC)%a)SyiR>qDj^GmqBZ zwO7fN_QWHGFAsYdh*M-0tQx1wY$QtiB;y%|EY_Y-z&D!5VDbsVL`xwo+QapS<3jzn zG%pdDuB6`Z6Y(RMm}?=p2h+qdfFKeRil;uN$0>2aJ}?<5!_or|!ondm9Wsj~MuPj1 zSV}W`lJ*RqMRA;B{s;slUO{9(0pKudIM)c1GL?=6NWhs06_hM$U{v^!SW7U9fPjc@ z1*IX8T&Vs-j}y*RSTC$!a9uhohy#uW3_(FS$Vw1Lfx1Y4Qew?um(yNs%HWGlf}KZ* zzJ)CU9Syjnqb}4hC21PC0sb94`jq5-I3=OEoVsIgWXG)Sx}hn_^inqL;4JCXwoorO?9qr9tSDJOyFJd z;sHHq7Z&FbtU26;C=70_&XkhGo|{m}@SgUVanN2ILlPfux{wqkts8clI$bvpNF2`K z)Z`b#0q6B!d<$=9mB}(+Tq;^WwvAUyd!I<%9w82zp9!v>DlSnpnwip?R3EozR>!Lo1 zGf2KeRs|6glKc*wU9R&ynSrHsd7=bYFkN`-%2t<|kIi5*TvA2FF zk)LGKn6U`?mgs&m7a(@9h%FLhk#50EAX+F*CBmmlTp-*l*pDJ~`5clEA_ig?tO?8< z^h;@mcMswKhdP)QI0fS*BT9}F;qwxU=OHV}xAIEeQT8Gcd%YQijkr7cJa7{*RdxfU zX$TD9NNPvjQxpf}A*)!jtI!+GNT2Lt#H+s~`TeqK7-as2bc>wb#!j}9Rm~UN@ESMd36etTsc^MX@4T&`1{^ENE z(3GQQ9o{1SAW36(z-Zr$pMP-1XdmO~KXAb~wmGakyM*qHY|&poMX+>~8z3D?Y~&%Q zP)I`AjLv*_$b-bH!TZr6 ze~cIulBfdhpn44d64e^)Syqgz@k;9917ZcxL@RP zSdeXnA7nx5ar)o^1I*4R2V;7`H3MWy|32fI0ZJyn?d+os1x&V;bY@-xMI=+ic7^Lj zRxDH!oF?ph94RE12prfLq-Gh2|+h%K>Y{na3KA_>i1{M;r_%Z zkEEi()q-ynbpoA3GSjs8!cK zYO`BNZ(i2lUu10s(nm=Jbmn-+sR@=Xs5az$sK+?Fa9I@~!e^@q1M8syY?nV+)GTe9Ui#CKBWEv|-JDJ}<~vn{gI5ILIr+3Vrq+>zB|{!KZ{3n+ z7Uh8~X)O}--)V5*)M546m&+K z{Gqd(4R;1TazQ^=A*f^s4<}`@xlTUQwtg^>&;17?pV?fS4);bPM2LC7U6h-zVmK$- zSB#p3(}`S0;t*RHgcjw6idv(775M{g%;y=fB7d8ME8qfi(B~PNZI)EHl@God!rauQem?r9=}(ZB?dlsGJ?;vg;@sRUIJuSo79c<|l}-j6P)rRMOCo!lPbQB4Dm&=<6{@P62F&;a39hZ z>@aA6D;yRCNM5{w-Ci&kI2nB!$eoM@Oz_-^)g4qRU?tnRo5UN`DPYoSug}W4&Y(^K zyEE^aJE&5`Hq4z^?LnO)7F=G(8Pq9cU0wIK;IdsSn~fTOF(&>n{9`G&?jYFAg6 zR5mG(mrBN8*3SMw_!#!E#z#8E1^JjovLT2v+=n7;bmw&cYvzFp{qyu|cGUa_d!_vJ z?OgY?{;!@a$RP~~Y$iW8*J@~@u*9DPJ$4x>Yh$b)BHGQ)KrNTHfFjca`X+BU23-fs2U86{62kD?O4_LPBylQH0$w-@d3abDCV!nu&N%1?qLMdqs0odH>pdac zU;5MLVKcKUb$^y_1%YSHVuJTAct{;Vhk#7*;o@$Ay9&%i@RHn?{>C!8+CZUifD*1A^+6rvOZIe29aqm6vz2YtC{3@PJAZb?&_doquD~Gos02_WP~erZvBVRC zlTCmdWD&4efds~SAImgr92qM19btx1B0XK*z)0y5y1enRXpc}5{Rb}>IQf1 zpfDla{Wfkf=p_YGl|HXy{XIj>gqb}Z4uqusy<^&)?#j|!EO3E3H>;bftIl0J z+ZE&gL{7}y_=hJM#*!G93sq41K&){eNjO+UZdRrk-y;i4jQ$m_g*P^UFhooG-8{_uhx z&Ad)Hs8b}gE|=nkAq|Z#;J>&k(g}4Xh=Wa>E(5f z8r0#|AS_|>8znfk#n=Phm8;&MH{h@#V=8wO<#mo6ssmn*zjF|-E9MEI+R)pr7~(m^ za3`;G#Gp=*bRT)0!v}Q;L&gdH;y&oS&gwxOuDwI>0;Z7Z84H#?9gtuMK9I{;$OkTY zOY-SidDyx-@IO5m9z=)9rF0$y4(`gp0YGwbiLW(bqvh=!I;c}5v0Yy0kU<^f#)9`F z%FKiasy_O;5xrC%8TTs!;Z z>NO|K?x2tAzE)Hufddi13omogiuy;hLpFr2nOaY6&_7u~PAqrWO{Ys_Pwasf^uV;* zt$#pqv7ni$KdQ}Mpc>^bIDkOb?7~v1a)>|t2}B1k7?4+J*9|FlfV5qp^>t1?Vij@f z;YFMOBwc_*d|t65B7kEb?QJT1x>}m|9ZR+RlX8u;JKS*?B@!NhTr!1!g((7h0%d~R z^^mPx`zAJUk*z=6ag0t+)TH?_g;A3Y9rA>1Tll}wlIACigySIC&$N^^tc()$;Av$* z>(b+Bq@M12Un4CN4OU13iALJ*DsIkBw%$W(V65QjF^BXf`FFe={*Z zvYDl&mHpX)X=PEgZwW~6JHx$aDi@68A=~`IvT(TfaBQ;AFJ0Gr1QU#hb*^a`FYRu& z+sI(W*ksZ6#e3d21*HFjp)l<%_zpXPWh32phr3VLpfe(uxk~`%S`3IwfDD0|@UCOR z&afudl6a8z zaF>h3hZQ;%hPw=DZkU|_uhVZ2ZBfEwyPBOazeP!c0b4+C(Op-l3B6$Et@3uZ_H{}y zg7aq`-SONe--5{71*2LE9Zh1B&yOVcxwymF2@WG|?#|CX|DMh6!|aQ298{vvU`dbZ z82P=*zk*O)eRuUIlIXnd2&K51)Q^Tv{3A(sUh^i7t^9n#tzA4ZN?iQS zW4Df_V<&LU-A0vk^f#kltL!rR-qF(PN+XJ`l`{*>u+nUs%%&rZ~ngerFBvA zwa18)zrQ{*vc*-k8%N$ea=^&)%B_v(%D<`wM9fzuX1;!1b;s*w_bY48Fq9z325>)+ zX?VR5hiDY==m-oZuA5}L(Emz0=Qt!W!kpzAE6I&R1_t5RqO(C2lbBlMA~!zh_;2of zA;AX{lSnrp5OOZSGe+^m;v{pw1^*My@K%~LOj>`~y^w@`3BQB>O3XS}nm}aY9f11~ z1?|$NLSG{ioE$9#$#G)=949jJ2+fZeIHJIaM26BAdnKLJ0;-y1up?%Qg#MHydWxu9 zBHT^b;LvdaND@WtG@M~bqDXd1#d0Gv7lM(W1zH6(LsT<#Gr4M5{jYAmCuxiz;KDnW zEe$UZ#2dwu%c?vRswSLFr4yP&3A9b&>mmZ^C^T%*Z6##ZYFokpG-1y@!aHY5v9N?3K30j+U)Few27r!uAVF2YFkqb>%rMhH$M(`6r z4{{KoOQUY{P0&{5t{&12k-&z;FYnYvah7oF9z#S_Co7*ALIS#p=7rNvkGaK|%S7KQ zC53fKrX+~(WH;iSE^NyVgnY>6kcLT*1C>~vuS+KW3=kPbMT5_s$LUAKW3XW^U5&8<(U6T+J zyicoiTq<#@loW`7oKG)~5vUXOOCjnlbiKJcKJVzl2B$hsH!t?z(gh4ab+i=C_W6U{ zs`lsfj8-eV{e1Pt?6+?q==bqYlVbC@3sfz8>se~M?Dz|a2EOcSwWMVHHM{r%bxaeVIwpy{QfXBvHes;u+x;k%gm#a}PFF1>CsV4t* z)!E8gT7XxeUYk8|wz?+U`=@G&nI~{6;F0n3G&bq|r%z=cI!7Iu-ExLnn*H${b^Z$Z z;v$BUIxsuq4QkcY!kD$%FMFR1{9|XRgEkWuZKI@&iI(egWe1wtdwL+U2Ze)Echa&2>L_1p!s zyX6DCSlkj@kN{)BcY{Rc}G^X3vgz(9j=|?~H zJnsZC9d@?sYX`kuG`p+r?aQUNv-=cDidW=1M4UP=%kDo+ZPM?oLwE$kH&4~GSE4Tf zpbw~e@<5m9Q}tG_Id*mz-Lbyogy;_PND${kq7}{~Zq|c^%ZX2-Mbg%Ah7w|12vZw! zoDdV>dF#k=(g#@<>--OS0Wmt}%{@*w2#yaPt_~l3m@E|Sb~tu+=iwd~Ng7xH(MyJ1 zJ&-6NI1VwK(V~b87CULahf*SUJM>ops*(Q!XHQ;FAc4b=W~g_1UdIdM<|~9N%23aD zt0QpJFsK2*H>|aDZJ{N-e4f@$dL>q?HbmKUyYbN&(R$TrV$?<3-TT;YC7RPnSLn2+C0JaqtbVTwUVQZPtMTq}8k0HToC)^HokeDr2cAKnBp>Y4@s(YezOs+mBDysmzI*enWvS8N_ z0Wi1_>|uKvMk*zS5g5{*I|3>lJHN7Z6iWM8m}|%tg4@@TUDw~C_h^MLiys_ z2kWQSUs>O*u~Gdu^*hI}82#q>$>Y0^Z!-4Vu{+1!$Bj_0G43>eqc&E%mfiAb>50vNB>ZN5BEx8m<@Hk=C#LtFou~9Dq_3CdFkP-*mL70!p1=enh`WUG$jK#y zD9_kT$T040k8w;8wq^rSC_~QnVj2H+NwOFb!Un_-D}$w|S#lvfU|qeux$ei?YR<_NxD!)*r zh)qQzD!EhYfMb}I{iL%+?7&=o|wo zA>%43S;Q%8e6w+4w$o?TfvfW_k$IKWnrv0M=Mw%1Bod@Hd2`6mgvCuVvOEV|5v|0y z%`nQN**jNDTJColMs@r2x{pm|AN{0yOSb50wW4Aem1DBtPvm{mp`t~o?#6qwEk32D zSM%ooEuv@S35<*Q?G0|JEx#*$#UG}s)BIPGEqaPXMrUNFeM-HVo|bM;AAHBusxcX+ zBD@OY8tR;y{r0HRRypcb0LxNcJ#7WBEY;T21#Qa4ZzuBu@ZyjVDGcG=a8<2bIm#W7 zZJR`na@ScA)Jv_@%@;tuBzoYD`2kV{6VEeXaa*$+a;-Cg$l@IK2qlsPfJ6fK!hkNv zNFw7m`nwAFDak7*2S^b=6v@VQ$%VBIzcoOeEbPQ9b0>Lx~}sE7#$IK@yHZ z(qF>W52X15X*Ke^X>zRxSLqTeM6eqNEm3eJ0z}pCaG7O7bSpN(9@e*-y8XO~`}mSjg`EBK+%k6TrvZ_+8omqRjc31tq_0#LGY5sG3v$3bgX6_#Q;MiGX zuO%DKpGUtndX?HheOawluP;4Tx~cTeQdrw+bbpki`b8|H1` z^%M9;KnJf6!mKq8CzH%oM5w=o(4ENLQT1#@*Q^cd6-B z`u!3SDfeb@X%(J8GH4=;VfAtMHqaXPYmkjmtzXv1<>Gz0&O^s>bGby1Ym7h&*-)Z; z;S$%Fpc50JPKs61O(>m`B3oO+bvk($H;0gSjgVA=!zJ}#=d#nkHLq}xXC!n$Qzdy4$N*R@w~uf~GIEIeqG&ZUldQHFML4Ewb z`+jv`-)%%XSQl=Q&?Jac24@c>OpF}qyYuLipwee*=jMr6Ji>H3Ge@KuY-{hmo^~cQ>!1dxG z1|-DyM$#5ZdIBRhzcwxGc7?x%Qzs^~3VbQ?(y^}*pyY*+Y%E@qYat}`;g0S$9tuJ` z7;_0ACjl|(PPjxQA_pniY}6{ftcU?|JvsmI4dANbLIS)`9JB-%6Kf6><_2c2g>aPs zd?2Pb@pbA536dwP09l%Qo&?;Gho+a`AN)osDA&z7tt+ z$7&R_^kMAiXhqg=5-vIS-ueBJx!1}TPn4$AThg6QY-La01KfYBNVbuC)qz^F6(N5c*JelFtMZ16M3fsJ zPTzcTD?R&!71@D*LDMnsCMEoFB)dO`O_i1JQ~SPJi{t{?7L2RJbI>$Ctvv^$n>QCV zwy8Ol&y~KaZZtl-@@dTMT{*|9{EqH7CvPrF$~Z@F|cP zT2cgPjirZ)jhvLI0GBk?J&TLtdfY+N3zK1z2^o*zlnk;|;z#khfJFx^&jbFHz!P!g zyoUhxfG?1qJrpFkwuhl)zDH}`XFTHrNJ1>fu%fsw*?uvj9HiV&)EFO}^ z5WOcaAsFg>-=zd$>6>MLJ3*o-_8cDFB5BM)FC(r`YICIw{(bWBBEJ)4Bbk$2vMe#Y zZRdHT%dIbYAN!;hVqi}Do7!oQ_|E33mQdfwQQ^sPJO#Ngse3JmwWBT+Um!Tw#(sVg6~RQCF-b>I7Yy~qtGMfl@G z;XY_m?dLCO{XoKf<`*;%SkMv%wchNj^6p4b$Lu~ut^g=NO;6?>RgLV})70er1mZJ! z9-Rvsiicxpz+XfqqX6hRNl9>@yi)h9@2M)H{uE-s4>=(?|NWsnAZ&dHI+hMMneVbL zA+tO`bdDuMr`e#p&3g!?mL&|crH7p`yNB*|-wQ!l#Q!&bVDUoCcVXTSJm{k%`8wv4 zg}j&}So>Hdx@W%UP-r^3=Tldhsy4HiI4=a0?Zz@lfE-M-8RO3g+UXt7?|KIGyni}ljJp8jr>bYG{+eN3Z z+3oc7%Wpe;)tc&{b`dC3M}n@Y4C+9XUnqs0PG`ENJg7qYNC@-rcV6VeO&y`RrZlKi z4hPeGsG}#X|P1^wLd(?dRSg zb+(c^SQcUqkgY1ANAwLqklWuR;~l9v9g{4$8@?=Ewb{Na=gyHzi=~nkn~o$6#I}^+ z5t}O&d~%h7iI7+iECHrpW?rlqgtMj2wo=C=rw&(x zX186e!j*Gp4I0A!T!_|{?e|V~+{(E#rH-Cd(iAL|l`;M8UzyZ1q>3H{vx}lZXaF~w z$t`@uoYFTElZyH8#kg}ZO4CIf9J6Zfbg8qcd{2>-j@c9ImyT+kHs~5Rsw~7kOLyBS zSUGpT{EU2SJ}vN<61jz*#SxCPCrw;mv$npteQJ{P^Il~ z*8zM{@?pvy1ai?3LoZXy6}V9dRn!ml3T@7vM5R11u8=z1kI$%VGie0#X#yW=eskn2 zjT`FM)jm;uUq4OY>y2RT?2fDF%Ckpu$_=u-6~VBIxV-W#Z`c6DCkk(|Pa+w5kn6VV z9-zuDIc#+H2sv}-le`tVfPt*93nowPCwc2P2CcX6`Cs=SPd+vHEy<^jt~6W|5_Otu3=@_j^hxf%ai5>#EpHM`1Suxb?nA#d&1T(iAzS4s z-c}EHtH^y`0H+IH?whB0%bSDzLU$y^+t6>F;%(h;yPs0+FzL*E;H#wc1s@>wXDn1# zfwZCM3ip!`c-A$EnMu8YSD1{&taxEn!#`!a00}`}f-j9swf&%<;;r8r54HZTEnXHB zmw{#LtybhESXaR6cjwUI?r{52411aBAZ(Q56FW!lC}MU1!I(^B?Fq>VBUnp<}aheBvM( zr1=DI82T|lE{I?UA%h?k!m-e&!zx#83Z0 zp5X1k;m&Xy>Vhb7A%#i_Pi*f?OgFGVidDWu2_^#!qv zv7vcedFS#6IzoTlgZ>p7`w#b~$X%iZROEhY(tcZ(Kc7N&=s})BcIbh`kLy$$`we%f z$UR4e^lye|MD{UL(e7jqZmTw)ht(wdr=PxH{@;lL%d?^EJKUEdE`tJkeS3HfgFb@; zXtr9vaUSHcYwLdZ&24areTMr}?}P;(?z-i8|%NwNNdL`E;3 z_-u{b+>j^Y?7nuuWxBEAV&(>_#fB)je+=_(LT;iT26PdUfaV3kdkPn0KO0Iz33)7L|Vk(&U zY|W;FFToZj%{~Tb+rin!y^G8EK z0f}Uo_qbcl;wnGqrOd^~g9f;?kgLfhoeAmU@h=mUL|B#Frq<<73;e85gJ1j$cK^213B_V@(}=GF8(9GG|LqSI{(FIaA~1v^qqth_&KPw;h+w8L6gvcz(ky*j4fV1 z41R8ukh|UFc0oIglThytH?ZG2Fth@vfZ zR<0Qv)G440>m+k)Mh8{6`JTHeVe>$|5O+2NO|C;FsRUP6a`QJX7QuVo7Hz+#IjF;B zk61zAP$Z&Y4uXhb0)pZaX`~sZ8<&3KSL#Ba>5w=yWJ?fMdkl>5x?fz4$WwU=5hgz)rJRds& zbJOWasw@l-B1)l`$pw|%vubl&E#@(U{i(ZgNhGEWcPu}iQP2SnS|w^|X4 zXcOL+$lb&c;|eK9^OiVxU#FApH>x()hI#(;v{ChndAyCJq}igyr92bpQNw-VzCi;1 znXOo)8x$?(4XunD7U1gn1bW6~MK7Xs&0D_GxFr3^DJFWl@&^N$V!njH6hwvLM?F$a z8>Rh9OVvNrgX$Bt^{PLs-dat|i>oJ8cd2ez`IX$aS2?xxt;#DaFE2k+{zm!3<#Q&# zF9|nDx3OxnDycTqK@;zoIBuddF;=^zcJug8>NnJn9lxbvjbA!`e0_9$r}3ASU)QLQ z{c`N9jU&gd7&~QbkD6b)a@X1urN4}AI{LfOuh*i{_m7@Ay65PY%|ABpX@0cyrn=ia zS4}tfYc8uls?Hzz=g4c+ncgqWsww>p7WkkU!HP{U%04Se8fcGM5i1$VE5WGba~{qk&P`sF&E& zfypM?L0f|g4SPLmdtd|`#6655AivSa%$Ei@sYGS->jU^Yc_ERH1##u_)8_K9BFjQA z0xKo*M4{Xd<_UUf2S>+f~ur7(a2po372sW}t$P}V@9nUsBu zr!1un-0G9)Z_Ss=e!eY)F~CqlAIX8>=P0){^!1U~Mm;t9fzBM=dPcM|eA!noHK0GN z2`J-&;iMu+EQ+ij(I0zJ8L(iq&76U!-ER-?`Kl#}geY}KKc&lrxTQp~IWwRJ5Z$pl zES)9N`2Ez&Q_e zK%I?n%wqyci;`!p7dUsqwKeh+BPJ!+wB1Hd}FUEeb)`nzO zt44oe&SYIgXG*18_9!4`!AI8t?F1z%@d^GRGzRy!G8)7mQwVvd5xQ-8Yc14 zqR*Kx6|ffrcGyHB(<|;!IxSfFL`%-+XBRyC=HW zbTla=-3#!!jw%6}4<_eEVwV3HpZOjdIK6K4YdZ57NnlFRh^e5EOPRR{wzq*Oz#4v$ z@HGwUxe^b2Xn=%SbXdso=BW9`=w+9hMge@R$Px%AgGH**Eu-y69nB$x zz7<_*`Vc@CkFGLj1{CC?_gKty0DUVuSNzD;=x0Vh`?lzTmC4?1Q8N78qx%e(F2oZ% zI$%fx-+IT&aQkCd>4S=Z%q_eL@J-(WS#X9@{%Q>utVGfQLnN(eUW4tHVEg zY-UdtK`yZTB;SF2*cA%?2iP7CgUdnPtsC`*-+b)6(SN=%dT{t3-hy_{hexZ3*^ExS zF}m;EkQ_wxOQYt|C|;Qe%BV-L8@>Bzbl}_&2k7YR;r$Q~1OPuX_mbT9#`3Q(?}j`! zzjzA*XGaQ;7G4ak{=B)n=N4vfo%!BObNVaOKQr|isN27cg#WSV$*7(CDh(a7t}u8Z zi4}|MAg|JbbMlNvwHICYBO&WXH|>ZnUmwUJ!?q2X)ka`Kj;s%4c3~R{tTM^&)-{rW zg>6)qCX6s z3b>e-2hqGBzcam(6Os%4sCWi)&I6fE*d|3#NZ^`S7K`Ln2#Wzf;Bt)_PN__YM$V@> zx_$@vtwAMi13EQsPfU0q!8^nwh)olahGi1xuF+cBC`44i zHQJvx3QA1cH}L$lQBa!DHrkgq3JD=x_x7fZf)Zr*N~>w3pyU*d@HiWsM=M`iog9ul zb0OyGJkAE^rWYU&gXI%+t76RCP~j5@PvTwJg>2BMtJLb%XK{f&=>rE2JXRzC7lZ)HQhLSm2I}D8K0l;GOK;2FyWB44CrsSlP z&0r&ch(+m%J!uA;qD--xNxIn+tge+w^OSIIL9r;6F?Pu(wHq}r%=y6qyr-n;kz_X(At>oHRi)!O3ZbtSOka&`Nm~N z@k3%Yb`B=?lp0*}lQ>VA)?-%LYd3x|`wIZ3gZh=Ny?o_Km<#C-pq%O#j~3VtUGOhAq%-wr;siQzx1cl)>LU$RPK1K z>AKS}Ch#K~AwdTJoso{sJD@$}qru(5Z^aacTy#*lU^R?w*Q863&9jyeX@yY4&G)~? zoWcEs5a)i4&hR%Fox$0S*O+d)WI1lA;LhRwHmUsq3ZXs+DVbe9g}I9Ic%3e_{K)}| za9RF-kqyjZL8*&XN5X>o_Z3a|V}FTQ^{cZT3E|F%1U)H0j2Eh06|OJ^5ZlkZV7{aL zeQcL?-Im@o+Q>^1PnAK(kd`4^3rrVeI%SP2`C8I8xQ$MZrn0@k$=TD^?4vOU^zeLt zIN)e&vm39OzEHji)o;$_ikwxgC`r)6o9nnbw_ z9J+yqF(p$nvOSjj7R!O84=AKFnYEj^#GfV|NK3DCw}(wZ$&4#Ev?*Wk^6AaZoM1r4 zJ2q~zowDkbSinG_V2OkuOPYsN6_s8n+%PfG1Sc~Fye(wA$=&nnl;#$d;eFQx_;a_c zOf?L(!f{Q=oL~@thVOTy>6EXVL-=DJkjoGV7^I4c%7OA?V?r&KxdVVM7B7>tbJ@Ci z8CX%AA`lU#1kvF5hqlA|yZVDLr8|HsKlNH92h$@ofny8T@6eHrw`6;am`b$=QiuE} zzNOUcDI6)YU2jn|$3wXugEgAGi5?x_pT~X6UY`v!q@%QctD=0O#oJ946yrbmf1sz! zCoOGKs*Gu6E7%lJ>WqKwk@>Ua}OWcxM54zdvKMg z2Z0fj?gI4+Fk;#yBN58r=^;Rl1kU6#!IlvMp}%UBK>hPZ{?(3Ie?Vt!S-x>wv+Q0& z{H^)tpoR`y@g~!47nF|)(NI+Y!EkTGnBoz*n^B4D!>f7e%03N>>}{Mcgh@eORc4-;M+4|X z0Eq-)D;^!ZU00IBlY6EJ)^LNk$1T9oSOJjlB&$aAUcE~(AJd4^@OwWb5BYMnF=Yh8e0Yj6O07l^W;wbj@^ zbnx(j%Ks`jc}1Hd)GCRW4pZ)kXCK*z-je}F>xy4 zF|jZpAw@uqNHvBe{_b5gZiG1po9h0{9gbIWgEd~e|+bN^}fftg>KI+6bZhxWNzQ2Kvc_ZKimi>S8o5@0?~ zzqCkK4V{~TPy=YgT@4vyFZ#~#)K~5pZ!z3*T&ZJsm)&3k9IiN36tlY76NFEUPe-d7U4e{Cj^3>VrkMTsX z9W)25vV7)%Jli0X77m}XajWf--Jn1{>4MaGPJ zAh;d8FuP>Wz?6r~fSIit#mZ7P1;lQ*%GQnd$L-W~f5A-87rVm1rUeL(kNoY2+@qf1uv z_HFXrW=AURp|PQ$eH?1LC9Qq4>A%O4R|I=Uj1waP79r63giA~g_@SfKMFiHwP&H&F z$i7Y(9(mC&+XpkkOGYfxT`2PI2=bo7T^3222 z4^4ey+k^R!L?6iAc6_6Ad4ImYUBAUc&Pzy@h)h75Sh*?KmfGYKUE@^-+zfyqHqRX~ zT%cnnvLGXwfug7=CvaIP{UH+x)gsjfsKy6K$TLIu3E3a;yH5XEbFWCl^Zn&)hhl`! zU=0vru~BAa)Fy@D6`b+AJmj~eI$JI7BhCLR)TzG`0)sa+!4FR33n9A^M)weUdEw%=_g!YyPg$w_EZ6{YCnd z$^#@s_7v7c0;LLpN&~qKncUU2xfYIFHgN%fLMIR4em(h}`sm<-Ji^W0FrTkqu;K~o zp|KKiVDA2cZjhG-AeVx(9|_H(Gd$986Qa}%f<%c-wiYOq&>1$_q+VboLMr3GNHf)o z>Wse@fy`LQK^M0NT*}uU7}EgybJ-qIy+N>vpI%~Q5`BT3CWWl+88}4rvvHsUe@}`E zL<|`QGG|b~n4J^e{R$(FU>wMBv6H<$wYWC={^9ZM;d9Zy>gta^`&WIoKf9$nWwOq& zHE@5_rO4MweT1A{%>DztR@?!Jvl77l5Qq?&&=Lo*fN+M03lI>jRXkB!Nwz!aT#usZ zeEPKb96QBE`?Pex$V&8QvVGwpl$hqQ)vHMV1TqARDm!teBZ5gFM3@-10(?2LeJNL= zj9h8nD%}ZvgzDzU5g$#N&j=^%k~IrBj#DMq9g;s{Q(n=892CElz>4u!<7T| zJ~_SSgIZkfzA*?HG9(Wl_hF$mvB@70m7ht)zE=b0~X@zb%xC7EWEtBuLD2TKcl~4${_4VA4i! zA~;9AToD(!HEc{#yT`2n^KV_}z(A5|woQUbOB<~sQk_bNK-!`7g~kDVR%K+H(0=XD zkz|^?3~w!v9CElAh-ejh5^Fs&SJdQ6hii>1Z2P6a`s20c^fd#e%+w!HB&~ zFKtsYgu4=v95q}m#RN_X)sjlglVT4|ff zA>Qz!+q&z6X4--o0A&=08s-&@XsV&GBw(j;>#3U&Q+A5(ZM!yY6BN18v-QD|v`tWe zW!hYmwh0P>Oq;9IHbHTUX>(QDCMcdTZLUn)1TFiP?++|~-SYka`+^r6{P@h>)3;A8 z?!+5Ze)-9OW;XfCsmU|>vshRgyHBa zXdgoCiqTD#$*MAiTz@SE_z}q~vh@1IW}`tKOOnnmo0y*AA!p`5$w+YfES;yQn?KRm ziy?u zdnMhB7GMp5P)Q^y|BW~W${CO=shI=qgmoW4uYB5GAuR)L%E^4G8#>^(IR3NQ;)v1T ze_pm@A-*|E(UF%$TZFRod5K;j#6!GYzk=bH!J%YnzYnDX^vRM{B zlV#a+sQ;X7UqXDAf`T)*+fi4lq^i;+W_rNO3;F|gALsyNIU~?c#+PwoOwP60VF4v# zBK<_BGEs6E7CvgB0a0J3Gu+Bl4^o#lpT`Le^Vugfw)Ict-lWQ}lu?0W5r-8)wbBU^ z(-e|Pd_hhs##AHPU9_SC0?JE%W2c)Bh)v@Z%VN5t&*NcZ{_cm(LjNc9sJ!$mk;z2M zs(@IvkxIYn`rOVj4%r{bykGVNWea(~X8Sv`W2eVl)Sqyzo$yB)$U|(};~}4e=96 zCx`_|1WgRLd&5UBG07iYuGk=(;x!<~2b?U8J21TGmFK+glQS3GlG}d6^8a4$Ej_XH z^2IMK))wwZX5ilWZ_Qse_XkMLpO`&o<^wac({DxV;OcE(-ga^RQ>Y<~qWRn{C=eGn z3Y$BKIc!}OYAtIhj6k4^#HuXX#vGvj>P!YT07!rgWxU*|*n(!DrB-JzgIP zijznH8BwR$)9%4i?{!=w2~F8XU3go#3bTtUk#7J^fu;d@R4^p?H8qHTYw);C*>_V? zQ??PY2q10gBvr(7r$#Oz1ST|K4jBA_p6Ixm{-(61+;0M|0tKF!oh+`N4{KDB?Gg1d zM0jBBvad5&THBP^lx+m$1U+ynM8w@JGqB32{>ti#1XhdeSQ+({7ez;O_aryve-nFL zJ4DHj^38Ow%FX1aA%+I37Pf@@P12jPjcO36a0z7~gAZgu19H$HAxJ_&CSAlI56mr={vM8{P0)ia_1c3CkDU$-5z~I4BL*RA8 zcv0aidss4Ou-efpukxfAeeGw?FFYOTX;YR4_5vVz7Mlc;PFHCP35;?e+hHWME#nRuC(QSICfJ0SF%o( z*>~k;KW%(wCDasepFybUH;sr-d5}FE8vtiRmlz_st)hKQzC8J})Jv z{MRTA(N|#?M|Pb2ANdoN)WJ29(=FAS*7>13MMj= zkp$xn=)K$|&L?7wO47K%DzXKWn$RZ}DVqR?r~#;Fi5ex)S_DojjcAXNa6wOxjW#Jt z3`7`rU}$(X;bM}rC4o)A(rFZ<-TG3XYU{kCB8qqFu z`3fHKq)XOf64{EWWFraKSXDwc2xRP&cC>SKTYifhrnws-^8UH6nTrHqlX6d)GXr2_ zQEqj3b7vK82egc2#JN9rn{7Lps+#-$=u3}8myhi_?JvRdM8G*BNeS6vi3zrpm@F=q z(9){_k7%}XM)M_P8xrj>y$Lk-M4x5kojvy%jTXeZCW=PpjQmeXjs`K!yZ*o#Jcj7| z=e{!B*|h>kf_qG|lgNF2c=L5@qe~x&_86U%kP2CpH;?YXuKdce-0k{zh$mSNAtBBT z^LaIAf^LO5$97C$7REivL0-XXhoURxZ48Hsx#+W_?>-!@?+K{GM$@AU@iI1#D1-u$ zd(!l`On9x(z6UIm*+6DUU4ndMwxKbBB!rbrrS7~({5J=H=5j_wEQC;*`>WB8-*;Lu z9WR|i zdGxVa=30&1`}Gq$Oj8JPQ$+(wz+{8sGRh>$8WRheQz{u(%efC3X`CmoS~<4Ov8F-Xl?0y^;G3(=6&6olcyi&9 zg%2;hdEw`0Z(lgO@WaA?E&S^93pW=oDV#U|)coV~_s_p${_yNK=MT(J&;7|~u05FF z<>q;&YHVaAUjaOz(!<~zzy|+9D~rr>7vy3ws+r#bmyrE$my{mxM(TC!Gx6;v4T`Fa z^bCM)Sdp_{lgX7Q&RsIMpN#|<_A&{K3Oks%&5pO}ymdo1$I*%zU1Yx|Z8_~cAF^eP z&f2?ebn(Zc{rdvk#TvOUnMZo!BP*kO?v36y+VSydpBXtJ*^cNGbw4Cw6YbDtM$Ny8 zR?ff4G(zx(^C9A?Mugk;-9@nfqOPCL9myt*rbiY86#CSB?QTp12$`3+v{7XYVWY8)0kE zFVNXQx)?$FQsgEGue%7wCS*Mt#oT@R(hz27?uWzc-@4b%v;knfXuG+2JHBHKxZiMJ zbc+$X3-O!C{n6<2SLIh8?U*OqR(aGtA-jh`a(L2gmLQ?3t7OpmI-` zUj6k|m=PZyop-OZ69(#rqB{g=aDO~l5%Gb%DkEDXz1J!;dzEuv82!eF-BcMs0j=bW zHe!dT1ecN>L0e=h{!KJSw1s8~fNG+3D!#e9MhEGMo%;g30&`#1&jqCipbK<3$tUF> z*lRROs8a&bV>=IFmB1|bpm|ZED_71pLXjOtD-$kQLm=g9!-zl$fVwNuS&P}E-m2t2 zJ9_Zf{O_zZWnwNYAd@T%abSrFX({OLnouZDwX3-go8B-IfenEl;)}R%h|yu{gbzcv z*&$U_ZdQ_Z?jxhmeGEabJMPEST#ZIMo||8ZbI0}XkfLKWZJyrCeh{tfyxHi9M7wg! zI~V`$!g~vw^KYGd{mlQGdTQJDXxE92sYnQ#b4|L`A!tJExP#J=LIOJ&BMOAo(J7Co zu*NF<=+W-!yg^`*E!E#a3IkJ5)yB zjq?9Tjg+_foQU`1nC9+;Q8OQn84czjY7FC^ySii*z`FqDuZSp)<)jS|Of|7_DuS8Jnei&&T}Pvj^FKA!*r z?jBqw#!gHT9ak-A?Gw|{9zUl(pvBtA*0<~d)wkT>*8xGlD7Nj+b|z%AI62K-YiB#d z0NVohW=?_O=SarttvSKLy=VKow)7`pOG?RmVmPDz_(zBrPTf30e=j&NI{JUD8N2$U z6EYatpUid$`3Mj&gyJpD`JQz(*OlyFufCeSa84F#Bix-y?#FBr?C>e4)9e} z0Il?fKfyv7{4LcP661KjwP5J)G@W$B8S>y0lLb^_CYKpJaYlWBWH-jEnodD^1srDSPo@qa8nw0P&@s}^6lc-q2u7e2o*T6o>Uzg#%I@MPhO zbB7BbD%?*_FgRJOl8OvfQ$w*LmCJ$W-Y&}wHOW(=fd3u z4a%^nYc}a=e6p);VU?BCno0gNBYsNhxQ0&@8o4cnqrhL7ADQprZ6wIKFh#VQ(dzK7 zJx;i|$etAMYlIW%vH~FXq*OPki}AfgTIFUhx?=dTC#TFZwGJ zZhZOus84_E>-$Y48j>%Mo*LeHVDGT@jaS$v0SW5pABOKad++c!zhN^f)FG_>le4lX0Pq=zGIYoNu3Sh`~&B z+wiado0Xmo5g3X-F}&o7cx5teIr_a}@hb;L`MaV$!(V;k%GC*m%e_tF zt8>3>o&sRm9+Wq>(TGM;)nmH>RB(+VFZ#74@%i8A<@U_URQKqQtZeK}quDsxxBZpd z{&3rc`3Li7M<0l0ayK2{sH==C8A?m-hm{z!?L)+eVZlr+4Tc_g9k>CIz-*kOP_L#jad4T!0t_HRms(o*|Hn6EL~u4f)Fg*QDX;9iWA@h{s>2FGb(zYpMFTmV^ z0t=fT0Ip1MP;aVK8qxECyanFI_=|5!Q_|fxW^yz`@bE5a+2iH{gJUX!y$@3z30UqT zmpKv_Hl-8kD%ofepah}99k6G1L6l5!5!9SVDV3UgBDp51?ruu^(KdqZUI*GokpsdZ ziEgq~#J%`50lK0wQ*YZb?l-09Xd6*fM{I(#5@dGS{4#}b8n>IwDCvIEiA`UfmxH2h z6yn0*y0<%R)D1{ExkfTOv{%CV3EvY+`rIa>PM#yUIB)>99$Fm84v~^*xJEKCw2cs; zziP-<9v(iQ^`-JF{p#Y@Dsock=G)YQ;H7K8!0X{iy z|E7!v?UhI>Vtqi?fE9~vJf_U@a6|4lfquvv&O70|C-Xqth#^D@k;RUejrwevkf_yy zhY!?H8lzyKaX+#TSB8ML5n3q|({QelT>fmMkc^ybB*Q-22n7uw9^~!dooK0L)XLMI z^e(rR*NNPP@19KfY$JG1NPYnSPMWHaL$47^p}7XR2O)k`6By0rN^6@k*0YUxhge=j zh>~*(p)~_m^72jgdEjk@R#Byk!0s9ca)kU}dDs1?%$D*|AeSW|)~ zaKA`idG0!rN`jJ23R5ylFvt0ANTszqZ2&xwrON3TKP_z(5}cST9oUpxp6gx>AaoPN zJ;X*8#VAcR$(4a?PsrtwM?v4w=rC`}Ezh>0I0^EIsj*uJL(jtpUe(}!fcXM2+#t7X zdw2B*FN$V<;?vM#7gq|&+)HP!p1K)L(@!7Y_;-f~NBRe{VQiY6R9A|}Wj@Tw<2=g}vxh>X4%0Vr^3VePEGnE9+6geP736Rdub3$^hv!uu;Ffcr*f z2fyd=k^Wk?%Qzq*(9D8@#H6O>}Hr*nPox8F(7%5>qd$dPzt)N zkjh9!P8{dSmOG938z|kDb(T}Ni>UP--J6nFSppiMv5jM!{!H|Y5ZO=XOQRp{&9A1v zoks827p)%ZpKm(s;P8wJLUrg6ysb*?BDfA5zO;s<8W?S46nI;M-ZHPvMGvaf+O0O1 zyEuRw2!ao!=_&o)>{3m7Dm{tp!&nS7gNIjh4IGs74!^p;FWY@W7pb?y+oMvfcbOTN zq0a6BTo{Y1rg-X-lizo*6QJ%VQiwU>)OvM*tV=2%o1P%GUY|(;>{hW-phj^+eNX>H zTaBRe4)ynDd(N(xb(yFOA6I$~mXqKQY+o}GKNh|-*ovS@C);zt2V=bu(^GOc+FJU1 z%n7Mnhy?!3Im1VIA`m5QFYfuy7!Eh6~jXSOghVneDH}FI(2%RRn zi<2k5Bfk)p{7tHgPqD)ZZ>Ucsz&S9cPk`Ik0iV%yFCy?-;^VP4gFmd+CS^rp@I2FZ zFUycLnWl)dlePq@dISXoRwUr@zoo**N`lT?$ON2CX-XKyblzVc6m%)}VwL(e_6*;7 z|D#FsI|T_dXrz9d{)ulG?RaUl=Yt2M>E}fck#1g^TX^UEX!ic;FHU`G+r1pBAKy6Z zP_$uwo=Y2sz%_y*Jf#iYkU?{+Y;w)6ZwYLXN+#{8zh`pIX)8D-7yTTmL@WB-PqL2z zBwkEioALy;Sk^d$!C>bJJrV3T<*qsbRDiWtHb2Y0pE>ywM+4@rlb7Tsig@dui#cPg zzi3V&_DjX8(>4sVJ(rW~teHN$TxU!e(!wBNjWRJ4r3p3AQzZ_P(I(`=8ujLYm^PEn z69#olI!_0(={$3SPv;>w)34edS-F`~yWZ=Wcnswtrfx8skbPmd5nTajW9o+37!WYi zBO|^#mW0)vKsjW1@Bza%*GK9)so0GBb3zt7?UCxri0D$pqL<+mS|Mq^-^xf&rV8qj_FgkdP*a#aCKF+Yva_itI zP)sNz@f5T_V#3-ry5dlDiti5JH>Tk;6d;+ffM_<@dWN5r-MFd&ld;r0svSG}vFYp? zR_quaeI;fm0&~A-vV?VnaIieGXK6k-wj=I@8(tWT~Lk8EFbBC zxITW@)=iiEy9TzjC7d>!Ew(a6$qZH5rop8~nHf1RVKoDA7)Hp}Uyd{oEUKJ z%w|*7JG3dtaY*)w0E2f=e&~=NLy{qxv7Bfw%#3CFqvEKO)z>Vc6SfRyL>B8T>4kh| zF#CCi)`t3M*0o%}VTZG39_mo7xpcIUz!pV@s;{w)i|jfG7) zAKCPp7U@aWp*HhZN!wHrAA!mV>!;Ub9c8!8)XzRyAt$AUq=|>N&HTyu$X=(-YCz;D z-Ua!cigcyGZ~$&1^Uk&u21Pu)__^1{)0^@=vNEzEH519mZp!t@%EI5=K$VKE@_r4&l`*6sJfufFMVs4D%ue8M0SKM|M-5M^;Cckq3SQaUastq{x&| VfKW&g2G*W51;Bjtq$7Ln{|6bETgCtY delta 4511 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 Gg8aV+8-vpT diff --git a/fraud-alert-service/src/__pycache__/pii.cpython-310.pyc b/fraud-alert-service/src/__pycache__/pii.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f6618292d0f831b9739a6fe9f2183567ce23847 GIT binary patch literal 655 zcmZ`$y^ho{5cW9UTrS*7r$a)bp`Z&=E)69@NKkLVL37Q@c;+CxStqic6^FJvD0u?l z0eA?+L(rwA+E+k`Wt^4Zgv3PtJTsY}Z^rF(nhTb1U%%7~5aN3@u8U&h5g&h_0R=)M zR&Wj=P?E%igwT2I4d zt(%^P)KTc2HB8};L_M~D%*Wql2x23y0AUF(LwUv&HUbkA@8oUqL40CQp=R5d@$PZdn|QBE|gqyT^p*rGgVjDr`_d{ue4vz#~~vzpE4YQf)j9Y@q-Mn z|3CB{j@XEW2*4<>V7!yKf{zmAd*FeSSs{l5qaDs`9CB?;+uOi8&z<%i>Oh5J7A;aU zH*I%0wsl8b*-aUEFE^qdx3Y&r)`sJecKG{Xk*tqF3NrrkU+$QVSuq)Mp7wOC@12af gi4ShyC5Bw9JoXsNXAd}`Z|Grs str: + if len(value) <= 4: + return "****" + return f"****{value[-4:]}" + + +def mask_transaction(tx: TransactionResponse) -> TransactionResponse: + return tx.model_copy(update={ + "card_id": mask_value(tx.card_id), + "account_id": mask_value(tx.account_id), + }) diff --git a/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc b/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc index 0c684e75a6ffea0d2846aca365e667382ff64e9d..9f40dcf37d75ec77614352dae2b4a913a6d7cc87 100644 GIT binary patch delta 2094 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 E3)$fYs{jB1 delta 1048 zcmZva&ubGw6vyYyOp;ByNj5R2_@hO%EiV43f`W+n0}=FKDOwP7Dcj7XjU?SNJF)&q zEHqxL$l}FQdh{lE_CN6KVXvMl2wuFZ@2w~ioMj#}-}iRr&F8&u-s|#-uvjcG9G~8O zXm95j`%Ihhk-^PXoY9+CCuX;RDW+s6+s!p{={nPKy7@-FTWA!3X{oX*w+hPH;f)EE z*M$~W1vT+pHe5aNpr~A3P(@YR5vr{7=<`(Nxg0{Hgx<2MqIcg8S2gXSZ@;RizKVjo z%%9w&q-GaiXnS>a=`n1Bo*`_PK1Aj*1Qib*yW?rE*pq@2u3Bc&P zkMoc2i}#CCzyse)L}X1Od%dO<9>DnG)%lhe4;OO_>CbgnT;+ zTcPO&)>KHWlIXOwZLWpu+SGCDOLHjxm^pZA9|hF@n*9`7Z5ka|y=+ae9tBMzdjR$5 zbUc~$w+dgKy7$GYn>o~;ROX+Kg)O($0BKXSnL+apP=SJjGX=VY3(F8gpxaa8?5Ye;_GJ?%;ETZ_UP6@ydudeZG(2l z&!h~^5!B2vGF0B*C`6j7nI++WILfQqj z&9?CN+y-`Rr@dl0s!6UDL^jxH8zPY=khqa)o>qYkR)R>UDK-iubwi~)k;zgZ^=^_i zX~U-(Cvqdi79swLz~synh0(=hm8_kmeOgcF5+2v<=*!gJ5tZLzE_mGK9+Y@Vl!_j> HhidQ(E^gq? diff --git a/fraud-alert-service/src/routes/alerts.py b/fraud-alert-service/src/routes/alerts.py index 995797f5..220aafd0 100644 --- a/fraud-alert-service/src/routes/alerts.py +++ b/fraud-alert-service/src/routes/alerts.py @@ -2,7 +2,9 @@ import uuid from datetime import datetime, timezone -from fastapi import APIRouter, HTTPException +from typing import Literal + +from fastapi import APIRouter, HTTPException, Query from src.database import db from src.models import ( @@ -17,11 +19,12 @@ TransactionResponse, derive_risk_level, ) +from src.pii import mask_transaction router = APIRouter(prefix="/alerts", tags=["alerts"]) -def _build_alert_response(alert_row, tx_row) -> AlertResponse: +def _build_alert_response(alert_row, tx_row, show_pii: bool = False) -> AlertResponse: transaction = TransactionResponse( id=tx_row["id"], amount=tx_row["amount"], @@ -32,6 +35,8 @@ def _build_alert_response(alert_row, tx_row) -> AlertResponse: card_id=tx_row["card_id"], account_id=tx_row["account_id"], ) + if not show_pii: + transaction = mask_transaction(transaction) history = [StatusHistoryEntry(**entry) for entry in json.loads(alert_row["status_history"])] return AlertResponse( id=alert_row["id"], @@ -93,7 +98,7 @@ def create_alert(body: AlertCreate): @router.get("/{alert_id}", response_model=AlertResponse) -def get_alert(alert_id: str): +def get_alert(alert_id: str, show_pii: Literal["true", "false"] | None = Query(default=None)): with db() as conn: alert_row = conn.execute( "SELECT * FROM alerts WHERE id = ?", (alert_id,) @@ -103,7 +108,7 @@ def get_alert(alert_id: str): tx_row = conn.execute( "SELECT * FROM transactions WHERE id = ?", (alert_row["transaction_id"],) ).fetchone() - return _build_alert_response(alert_row, tx_row) + return _build_alert_response(alert_row, tx_row, show_pii=(show_pii == "true")) @router.patch("/{alert_id}/assign", response_model=AlertResponse) diff --git a/fraud-alert-service/src/routes/transactions.py b/fraud-alert-service/src/routes/transactions.py index 115b8cfe..f3c4a0eb 100644 --- a/fraud-alert-service/src/routes/transactions.py +++ b/fraud-alert-service/src/routes/transactions.py @@ -1,10 +1,12 @@ import uuid -from pathlib import Path -from fastapi import APIRouter, HTTPException +from typing import Literal + +from fastapi import APIRouter, HTTPException, Query from src.database import db from src.models import TransactionCreate, TransactionResponse +from src.pii import mask_transaction router = APIRouter(prefix="/transactions", tags=["transactions"]) @@ -23,7 +25,7 @@ def _row_to_response(row) -> TransactionResponse: @router.post("", response_model=TransactionResponse, status_code=201) -def create_transaction(body: TransactionCreate): +def create_transaction(body: TransactionCreate, show_pii: Literal["true", "false"] | None = Query(default=None)): transaction_id = str(uuid.uuid4()) with db() as conn: conn.execute( @@ -47,15 +49,17 @@ def create_transaction(body: TransactionCreate): row = conn.execute( "SELECT * FROM transactions WHERE id = ?", (transaction_id,) ).fetchone() - return _row_to_response(row) + tx = _row_to_response(row) + return tx if show_pii == "true" else mask_transaction(tx) @router.get("/{transaction_id}", response_model=TransactionResponse) -def get_transaction(transaction_id: str): +def get_transaction(transaction_id: str, show_pii: Literal["true", "false"] | None = Query(default=None)): with db() as conn: row = conn.execute( "SELECT * FROM transactions WHERE id = ?", (transaction_id,) ).fetchone() if row is None: raise HTTPException(status_code=404, detail="Transaction not found") - return _row_to_response(row) + tx = _row_to_response(row) + return tx if show_pii == "true" else mask_transaction(tx) diff --git a/fraud-alert-service/tests/__pycache__/test_pii_masking.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_pii_masking.cpython-310-pytest-8.3.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cbc14f2d553c53237701ac569c8afb9e7d08217 GIT binary patch literal 9582 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 literal 0 HcmV?d00001 diff --git a/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc index 85021664e02ef3ac3e3e7a71696a949a1bfdb71d..4bb438aae8a2d584fb582b9168f32b902d381f66 100644 GIT binary patch delta 2698 zcmb_eT}&KR6yCf4%l@#iEbMN#Fq9uVTUc12m}1G+iq_Ujv84sm7&0)kux0J1piHT`QW75>vM>X+9pB!KM+GtYyLJaZ2bMC-0Vlk5RCi~6Ix%ZxP zzH{&SvTGxMob?y|{yGZ`<F5IHYe~BGXn9VxQYCkRJA%?JJVp zx?`UrP9N-~e)gy14bsBW&YLaAz#!V`5ZX^8v>-&;J!fcAx0RO9h&~!a6E36ygyRUU z2qzHQ5CnvFgbsvG0NuGH<;sdGdTBS>LI?va;yN{wz}J(w#(S=`JR_;9LbYLf3N1dg zWQsH|X{mnNgDW}o;$#nd=8v#XUEL(j9=d++Ht4{N=%5Z<>O^oMxDh-E{p`NGeQ*F? zMJo-Xr5T_SFv1F5CrisiqKagPAXgJv_P~Fly~am$nR%d_a|kc7%bwQYfjkpD&rdwv zWQ?T(OTW%4@GxmH6^do7<=rEvxKLvPjA>Q;BIB}>iqt(Y`QrO-Bps7b45v}A+{Qhgt7KU zq2I(eUhO1jQSv3o6<`r(U;FN-d6gk83$3L|FX89GG|?78z*INA#{DsJd=L$W@rKa1 z5o%?jz3g}Y7`Y0uj=whnQ=)I2pXxlk!{~f;q?q&_*z>OtVABVIR~!2w5OrR|C6gG? zvlr0C>g$^?9EIHBfl|DHqd_Hvr<@I6WLcH1vlJ=XN_+ra#xSo~dFGl(#ZfSLn3!zHG6 zMdJL5pyI$3R}j8y95?Vg+sW_j2!6kUa2COIo8XfRMwC4cb+Mnqt*oQz;Z!Zroj{*P zrGhHWY1v{y?W29L;mLRC{lzov zWozNnWC2bnzV$TxAz7`ZMPwSA!Nh* zYPjh+kS@)j?*k(K*1pUc`BQ8<-IYt1RM1Uh3Cyl54l2)GwRs&PF2VNr|mQ zw%h-^kaaY}H4F40#t*v-0~|QPR+f&0+0$rC+xsv(htjh*uzT?GVK(Yt>{+<~bw|{) zVw0UK_NuLFU$m|`st$Ebv#&d3SJjarvK#Fg>xLa{&Z=#L@TYx)$ez2_X3L6Gu9Lm& zvslZN&kmm8=Kx<;*^hoT!k>n38~6PI_Z{HA>;JFsRkLr)x=prKiENKr?pWmD8sxNY z-LR~~Fe_As(Ic#ji!kJI3D#^63PyF4Q+8L6Dp(7%d8 z*JzE9Tcki@)&ik~YG4v0x_h>q&1u delta 2379 zcmb_dOKclO81~p+KeBe>Je=5hZcF24^Kj!jB#qk;R3N2Eo3=_NPztfO-gUC_Q?u(L zM~MPQBo2sy6Rm`Vgk<$lMN!3tN*n=+6SqAfZWTzSmkRa7|Ia#(u@srPv`z4J-HtoC*A9znMBibHaW9?uuR@{l1}!u>k=7fzq=lg$%8N5 z?+_0jr6T)FSR!Gz=(*Jw1%d3QXHlL+2qW~deNXqzbyvAMF9&D@MW{#v2m=U%2ok~& z!Z5-J!U=?v0G8*DlCNlnEYedb>qMAl5${B543{y0b@BCbHLVziM$K6&qa=WmY>5^W zGciraQIo?2tgK1)G#FuDcw;2RzVrSPv(tecS)eXdbt4D}9t1B!oIUiNI5!PT*-5)m z(gU#FZo8F6$I8_i*{~ql8HlwtS$01di^$&Q9MHshgi&_IKOAa_ah%8anLkDrS)#q? z;Aj5;d8MHvyDVpX+9ElN+^G@UjGZ47#q>xcp~v44aL|rkLI}KiUF>6Vez*_5x&Cu4cbN;O|nRNM9_p2nWg(+Ipp*buw= zZ{UizAkNb(2gb=Vq&LXE4LrQeOAqlnXd{057JjzIkFs6_%%LXQL&YoJ2>x{FZB%F! zj83pWf^U+m;B4sq71)vktGw5nu!iT<_b=p>UV$^T)dk3`BqTP%erk_;R#D{|I`khp z6j(<`@1+;X?s!KDUcrTskA2e7YdgD+&Tbt#!>QeFE0)YDWlB{{?Ik40q)^g!bpu_I z74|_$dP(_A@qC_z`fUem2fdv?k~qJFM+FOFO^+W37VPei-9()P!VCg35wH&@o|YL| zLn4AbPAvS`Wjw(z$+3exhbrd~<`J4E3_dxvMA@(1qhtdrG{{c&Jlbf4|GfQ-NtL3Z zWK6wOG|tc|IP&-hG;@4TunX7ppL}AuHZ_wrhCP;&4=-beYS8Ncbs2f&6YO62Rg%G2 z51xcSB72QsX&U3g2zhWdvfYIomyiujXe!dj|x%8uFE7UzTG>n9e2*TE9?lylIi|RP(3?B zmZ)BqABc*+yUt$6u1EE&ZS2#5zNF}elkgMZoKp?d?clrF&hCHN*=w?+ifb?^l^alz zrAj_8WfZEWbyd<0sjN{$H()HJ-#+|YDkw%yFK*(-mQqynAXZF?YDO7mQj_vZRin~a zwqjPOCK+WdBgdPl<7g^;4#)ZW;Hub34IztwoVrT=xZFa}5n3+F7?yO-FiYS#orQqY z^?(bYY=CV?moq|&PD&_B Date: Fri, 20 Mar 2026 18:06:38 -0400 Subject: [PATCH 10/17] Wrote specs for filtering fraud alerts, wrote to TODO. --- SPECS/filtering.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++ TODO.md | 8 ++++++++ 2 files changed, 56 insertions(+) create mode 100644 SPECS/filtering.md diff --git a/SPECS/filtering.md b/SPECS/filtering.md new file mode 100644 index 00000000..884b39d7 --- /dev/null +++ b/SPECS/filtering.md @@ -0,0 +1,48 @@ +# Feature Spec: Alert Filtering and Listing + +## Goal +- Provide a flexible query interface for listing and filtering fraud alerts, enabling analysts to efficiently triage their workload by status, risk level, assignment, and time range. + +## Scope +- In: GET /alerts with query parameters for filtering and sorting +- Out: Individual alert retrieval (see alerts spec), summary aggregation (see summary-stats spec) + +## Requirements + +### Filter Parameters (all optional, combinable) +- `status` — filter by alert status (e.g., `?status=pending`). Accepts a single value. +- `risk_level` — filter by risk level (e.g., `?risk_level=critical`). Accepts a single value. +- `analyst_id` — filter by assigned analyst (e.g., `?analyst_id=analyst_42`). Use `unassigned` as a special value to find alerts with no analyst. +- `created_after` — ISO 8601 datetime, return alerts created on or after this time +- `created_before` — ISO 8601 datetime, return alerts created on or before this time +- When multiple filters are provided, they are combined with AND logic + +### Response Format +- Returns an object with `alerts` (array) and `total` (integer) +- Results are sorted by `created_at` descending (newest first) +- Each alert in the array includes all fields (with PII masked by default) +- Empty results return `{"alerts": [], "total": 0}` — not 404 + +## Acceptance Criteria + +### Single Filters +- [ ] GET /alerts with no filters returns all alerts +- [ ] GET /alerts?status=pending returns only pending alerts +- [ ] GET /alerts?risk_level=critical returns only critical alerts +- [ ] GET /alerts?analyst_id=analyst_1 returns only alerts assigned to analyst_1 +- [ ] GET /alerts?analyst_id=unassigned returns only unassigned alerts +- [ ] GET /alerts?created_after= returns alerts created on or after that time +- [ ] GET /alerts?created_before= returns alerts created on or before that time + +### Combined Filters +- [ ] GET /alerts?status=pending&risk_level=high returns alerts matching both conditions +- [ ] GET /alerts?status=under_review&analyst_id=analyst_1 returns correct intersection +- [ ] GET /alerts?created_after=&created_before= returns alerts within the date range + +### Edge Cases +- [ ] Invalid status value returns 422 +- [ ] Invalid risk_level value returns 422 +- [ ] Invalid datetime format for created_after or created_before returns 422 +- [ ] Filters that match zero alerts return {"alerts": [], "total": 0} with 200 status +- [ ] Date range where created_after > created_before returns empty results (not an error) +- [ ] Results are sorted by created_at descending by default \ No newline at end of file diff --git a/TODO.md b/TODO.md index e0b69629..08a62a66 100644 --- a/TODO.md +++ b/TODO.md @@ -6,5 +6,13 @@ ## New Feature Proposals - +## In Progress + +### Filtering (SPECS/filtering.md) +- [ ] Add `AlertListResponse` Pydantic model (`alerts` array + `total`) to `src/models.py` +- [ ] Implement `GET /alerts` in `src/routes/alerts.py` with all filter params and `show_pii` support +- [ ] Write `tests/test_filtering.py` covering all acceptance criteria +- NOTE: Apply `show_pii` masking to list results (deferred from PII masking spec) + From 9681170bb80affb8d581ab8fe8babe67f4a87d7b Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 18:25:18 -0400 Subject: [PATCH 11/17] Implemented filtering functionality to allow for filtering and sorting fraud alerts. Wrote tests and ensured that they pass. --- TODO.md | 7 - fraud-alert-service/fraud_alerts.db | Bin 290816 -> 352256 bytes .../src/__pycache__/database.cpython-310.pyc | Bin 2111 -> 2140 bytes .../src/__pycache__/models.cpython-310.pyc | Bin 4250 -> 4456 bytes fraud-alert-service/src/database.py | 10 +- fraud-alert-service/src/models.py | 5 + .../routes/__pycache__/alerts.cpython-310.pyc | Bin 4940 -> 6707 bytes fraud-alert-service/src/routes/alerts.py | 83 ++++++ ...est_filtering.cpython-310-pytest-8.3.3.pyc | Bin 0 -> 14213 bytes fraud-alert-service/tests/test_filtering.py | 276 ++++++++++++++++++ 10 files changed, 369 insertions(+), 12 deletions(-) create mode 100644 fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc create mode 100644 fraud-alert-service/tests/test_filtering.py diff --git a/TODO.md b/TODO.md index 08a62a66..e542e804 100644 --- a/TODO.md +++ b/TODO.md @@ -6,13 +6,6 @@ ## New Feature Proposals - -## In Progress - -### Filtering (SPECS/filtering.md) -- [ ] Add `AlertListResponse` Pydantic model (`alerts` array + `total`) to `src/models.py` -- [ ] Implement `GET /alerts` in `src/routes/alerts.py` with all filter params and `show_pii` support -- [ ] Write `tests/test_filtering.py` covering all acceptance criteria -- NOTE: Apply `show_pii` masking to list results (deferred from PII masking spec) diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db index 38ddfded0e39b6397476033857654ec8f02f7d55..cba1adfd04bd73555a1a0f2c4f0dd4e1d19cae0e 100644 GIT binary patch delta 44261 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 delta 6709 zcmZ8lcYMv~_rK5h>~WhGwMB9hx!H=+s!jQ7YsCm6Dy^zLN=t$W-A3j6w2rN2tsqjl ziWm_=f~Z+5iPEAmqG(ao-}&6@`~AIszx&7MK6{*V-silZGi`6xY2`t4Ys&nV%jN2r z8QhZcTiI`~a;sI`v#`b0QJBv~m@I)7(wE6PQpYtXGowq+ocO1ccW3uRMa!ufJjsUTJ7~BO!FrKb8aH#VaV0@%E{&AjIJ`P|6KN#Jk}Zlo zUZ>Cm7Ugq)Q#9OY$NZ(Xu*FBcD)w5=GGb3{50SUV^I)~MI|W-^uM4wVv&@X>oH28M zw?)dpWmU0}xhFb(t=uQvtKAdja^>TkYHy#jVbmsGAGUAe9`0Z4Qa=xoptyq8a<-2X zlSfBOb(|h(@qlvTEK)!TL2r4C~C*&B3{rn%3!v-GpB ze_W@fku*%XLK0-5U2uQw?l0|=hw2i$C)J=U<*NLE`z5z3wNuONGJOV{paj{u$`AHz zZGiob{i+>gU9l10=*NpILPpF~jdmDp1pE9Mo1Lm4>r zd=YyvPMpU@YZXU|!?EGwo8mZFkx4@-d0)&t8Y|kKj1+B-go{P_v7$q9By`1%WRmG+ zy(r9XNUxluD$ZoL zzcb@Y>?DU^&tl>w2Vvk+5+Md2mBm~6;avO76)8$fBgOfWa5#o5iqZqBc&^YR#uUW~ zt;7qX4M1m-}7b?3x^_zQ88xLtJ+^4)EIN!?~ zuwI&&+RDS7ZN%}39@#b1KdiE@hfr}jHG+&1En{QE)-(@E5!rnN_E^ zNiZ!yli%o`?oM>ay19Mc-eP}d53n2AnpI|Pvu0X@tY(&N{$lPlGtKwu7_*gG)u=G` z8DNYw+8Z_XO1)5DtdG+>kwf%#y{>j&E7HEzKGfdSLTI1bWW4%NJ*KWw)70*21Lbe! zl(Jr#ru0^#6)K-2OXV%{r}6;0P;Mk^Qkk@k{4ULuQlw^*&9Cwut|zV&c!U4~36 zlwFF0GT*`-;K2_!_UFY6r`)@BCvh~G{`L1H>tEBZI^Lf&{14=+w2D@0OS z1Q;KZo<8LxGLZzvcbn#zY?&Ax<`vsc&B&Q`I-KOlXKPXM#<_o><2ShWyz>nqC7gxk zRJjmCVBS0uLdha=QnWU)LPxZD%A|Nj~b(L<-D>* z`BX_#8Yvpjm&@dB@+_&EoFcy>yQN>HJV{8yNID-ydb!WKv)nTXarbjKbgO*6U21Q$ zXWA+DE4JJE)ylJkHPmWlRWomz`R06cl-b^_={jpv8ij3*#l|?JlTlZ{uNUdx>L2QF z=^pJdZ?7HK*6^F!WUU9UsYS3twwR4$omgFZpBB>Q3Ya5|+g!k|rvHd+;qP9i0N05b3e58O^AQP3`pOouyZBn(_9ZE`f*;kyMaVV$$n=rrGj|i@kG)qcN!tjrIR36lIxnweTFu4L+%t-dZ;cO)H;oYe zkzS&&)+g&dXn)_lfuK1MM$~(#{id+7*t(+$d za!DR4x00($71BNlq>&_tZkF0fHOLpPYrK*l_@_@&~9 z7ij<+myuWuJgDk0f4kp(lMvXl9F^E`Gg<9d8XUb#JS1)2O5#OS#lWWCk=&s)&nssNSv5~g}Z`mhRUrh6mES@vY_A_verRUq{zG!>a&2P6Ud!U z(nQ_;q106>j!g8z$ps|N@2!1Z7m^huU@+5Kd!XH7vPOKB>h-<0gp?2`a87}X%SdFp z?c&|hE&ol**(tVxO=pRu8H*#iOrjU)R+{WSOKkTh_b2ZD$O1IG%+9rEA;y}~#r4^*3|rt-70U74f2tGucN$=BsQ@;s6vkC5BRHKa=EptMB#KEZl^0Frw$>Ps44ZTLkLPy@~XJxtqvj zF=0xWulsrGA>vGf2#7vMdx|mXB=PlBuW#ciT1g;dJD$|F z2sYq9yn2G3hAm|@+#e$0+a5Tl?nUYa*onPMF5o)Ti;!%%@_aW}lOh7;-;qQ}SxZ&} zLfHeo){&1LI6nXcas+hVfcixYd;MW?fa|(T@hB_UR{1e{P1L3uto%W9-AV37Zr#3Y zZ|CvSDK6VzDM!d_tg-!`-NJQUdMdA><;q0f&#t0$VaZm7wU1ooACu?Fd)9oZw>(*n zP&-?rsBLwyYMb}WB59hr+?+^um|e|qi5h z4&Q9K2Y&Y4ckXo9f2aD7ao1^kaMufaaM!2Xao6=f;I3Qt;K;)}|Fh_lB)ofB z9-g@6HoW%aA{_EoF}j|tAu+;y#|z_+;Pe-dk~H70qa=*L-4>1z=0N`vB*1fes4(SnaYMJjM2yW((1aWzoH*lY)q0UNQicTZFXy7Cv zFtCq`&`5{k9k@>Udodyz&#-?N`8LqI{0aKT4@AY#Sm7Dz@mp*n$#VIf_#3=F`;#4I zE7>I0jWwWu|ChpH)SqOHcjpP;q#s{g2G)GT#|+E%0F8EuaC}KOTwZvGHg63G7*g zK_m8O8V|EB({N~V8IN!$jE2arFZ^Du#-~j0&ZVZZrJ%!{*?s3AWb?Qq}kq#iUo+0g4W+5SnrynZ0 zO1Y{##6W(}{vK)BAbGCcl;4!E+7>xxT~(8LO;#BQAM*27OSYKjL+j^wwL~)zJ`O1< z($~f^V}hS5Jk(2ACw(%uG1p=EgPrU+EeYc+Ss8F^ggSrC2Da< z5+6D=f~2Zjd3&UopJ!(5b-aiKCq|+MRs`{%Amt%mjB37bpGT?yUvy-VaI-3(3bUVL zJLwA>>+x9FU5y`u{vBByH13K6EHywk|9jvbwSaF(=#$vpeA81?^Z^?%R}fOGiS| znN^@frLUwb4#iR$IWm<7D+5rT7agJ{H?;@yYjzWPrfs0f`>fg4yMBYeX6Bi~9EvZO zYFrq%jQzZovA`Iu_LYM8Q00Qr!Kkf#rpEEAvZenn{qVm4kjoGP#}rLlRa@SsrD@%@ z2%e|@!z$!K2nNZ4gOV*Z&(7$|0>1c%5!~Zz-k$X)A~ZdWoQGS-FQ8sRad7%6Nmwh=M*(n(owq>yZ1*iaVfQN~F zVP0rb$m3zje(n(sJYE=!nVQ_a6iKK$%=Lz=KqA*j9uZ_HyTra{Gvy`9D5bqpQ>~`l zQVOhj#$juO)z+$R-Zl%&g-Ad-9heuS zWjsoB^F+Y(WjxLi0eazVQ7;_>-AS4VEjq9;MV1%)0X!%S0i1J^_JpyExmPqF7XxkI zWFB9uC44z?#2y86UT3&K1zdWaA&Wo#KUEE znBPn{e~v!=EbSSv=b(8`_}PPrCgg<6IvC4ji z*3~fj{PCDYJJmVR?JpMQlmYSs|PLGW}mSEtTEGh9GB#Aa;03TEKtTM9rY(xO|#NGXf827F#l!N zGyX7+8mmYZvXf+z_h}i;rL$;?T+ir+Msh@-s;`w+N~uzJsR4h=Pm`8>13S+%c%s~i zTt!34a`I_eO#oj?&EEyUGEwT`TkcAMuIWiVr*u)P*{AQj2S(|wb+{Ue;B1wvcJ0tLX zt{X5iNs5PqNm2wn*ucXXY5*ox&~!Lf!|6W(iR=8?R@xF;-eOTu^%h!(-|qH&=YdVv z{Rvq9nV9l!7^L3_)EW!SnZjQ;{PxD)gOcCa2XJvMnu)v_KNmNlr^$7yNBSdt{nsA( zGTLD91};OtW=?F4grysJtnd39v>p-v40%RYve#q4^iqMSdjT z$%E>fy>IGDUl$ozT{5Q?*`Nlt$E_)XmBl>L-~Q!vlVO1o{cbUG#)K-=h~9yC26qk4bgtb)JSv_xvKCQVY?2JBOZugwr(C7qpEs0xfgt zD%kfU4TG(PJSw1@%Y{4@F6ZIwPY&Wb^>Gh4dyq%N8%`Cea@=cpCFfc{SYKK1S+7|yII0aO)=7StHI2KFSjmEIt&dW!#UiKizn8N! zYAgP2d~^R~W2+BE($G+40UCx0PL=&P#rdh7WWQvqR_U`8XH&~Eub4lWbIl=UOS8)V zNpRlP52}6irTTcivmUJdp&ijyYN^Nt8aR16%+764GqN)l1U82LeQ2bw;dVMNKtIKR z`yRNn0|}(x95RZK80}7=e-Yo6UNn`!XMO03++fb*b*3+M*+J%PeQ9>aT-~>{AAe7( kq8f8t?$P)b?P(QQ&CQLNE1Hv?k)vnM-S3+-oR8!G12pn9g#Z8m diff --git a/fraud-alert-service/src/__pycache__/database.cpython-310.pyc b/fraud-alert-service/src/__pycache__/database.cpython-310.pyc index e08078ec88bfe1f3c4b157b92dd6af57f0fe29ab..3939d938fa6781fb6c45767f807528d0a002d9a5 100644 GIT binary patch delta 499 zcmZ9I%SyvQ6o%(algY&-mLdhkcHyqL5)lMJuqwC`K~X7!U}g#}n(A~!EvdJ5<*wuf zbl0w3iyM83;1lROs3$2{a0brxn|}`HHT#iu8?I{;Jl*$~+E>plz;$nSev7+Y7aE5$ z;Y^UaS=G3~&8}Xt1ReFNlv~{H>K&-qqbg?{kRRg?@?#vfNy+Q=50X{G@wGf#BY?k!cXr3L{l$ehVzYzq&SFy2HKHOT_*@zZk2ApUg9!w8R&_H_8 zClqdWQ1uRVQ&s4oYOBz)Lf9ohLU^Q4==VtAXhdbM3V>N!*OK}@xxgM$dXjOn{TE*l z$xyh&q=!ygXkz9!PEKknw$?9wajI&Kpkm6XO~)6L1xn}7xCu@9QS!8zM?-HNssRHB zuL%_BH~q=$GWYyO`&u!V;D)#q{<#wJzgF~M6xjqKl0^e!X>K@+y*gYE4l$fC5s@i2 Tnj?+eXwod2QyI$gFzx&TKU8Z6 delta 450 zcmZ9HOG^S#6vywm&g0H26hp+&MXRJ$L=m*=v5*!a7zGjt&b@*bBaIW9OknA%oy-@| zP1L4Ms}|9^KElw#Pg0#RH0WM9&;R+|dp_fDajR)rI>D29d9A+>tt@ngtLq0stpS&X z#1%mrtR``lvw>XJ1f6wilxtic$bBg5vl>@8pw{3fY7Gu0lDCFWr)w$Xdt?MY8N)Sr zq(ObC#C+^O!eL$t)z-y%-4pAfYBwE6*j`Y8WiWy>xMMmnkifNq2~AgR1htB2Zjk4b zKGM3`uI9rr0pi05#^_IoV@kxMS4)7|v>uD>fViZzdT|nQ8cA-5+B~ZY`%YXv?qTeamO*~KNgOp1|a#eJMed!7JKP$LbbC3)^ u)a{K>-QTSgca937zW2L#z3I3Xj=K@VO-APL8`H=PHd&6PV^mp$jQInP174;8 diff --git a/fraud-alert-service/src/__pycache__/models.cpython-310.pyc b/fraud-alert-service/src/__pycache__/models.cpython-310.pyc index eb02d481130de14f508a8d6092c8d034f3235677..c5127541372767afcd8f461a9cea9e4cda60ce35 100644 GIT binary patch delta 309 zcmbQG_(F*vB}FbpzJ(=9Aw?mWK~r&a3ZonEHcQy8L{LK#w-Q&}V#Qdv_N zZ5UG7fHad0Lkcrc2g@x%$DGun5}(ZClAzS$g8aPV)LU$cAfe)0tR?v+i8+3n>_wtL z(~87EggA)s0}-wuLJvsXV$RGfDUtwj;l$*3tZbrzK%ojCE@lG~Tns#nJd8zwlb7?& zWDK3`$ge723zFjl5*& sqlite3.Connection: - conn = sqlite3.connect(db_path) +def get_connection(db_path: Path | None = None) -> sqlite3.Connection: + conn = sqlite3.connect(db_path or DB_PATH) conn.row_factory = sqlite3.Row return conn @contextmanager -def db(db_path: Path = DB_PATH): - conn = get_connection(db_path) +def db(db_path: Path | None = None): + conn = get_connection(db_path or DB_PATH) try: yield conn conn.commit() @@ -24,7 +24,7 @@ def db(db_path: Path = DB_PATH): conn.close() -def init_db(db_path: Path = DB_PATH) -> None: +def init_db(db_path: Path | None = None) -> None: with db(db_path) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS transactions ( diff --git a/fraud-alert-service/src/models.py b/fraud-alert-service/src/models.py index 58b58116..c13daff3 100644 --- a/fraud-alert-service/src/models.py +++ b/fraud-alert-service/src/models.py @@ -117,6 +117,11 @@ class StatusUpdateRequest(BaseModel): changed_by: str +class AlertListResponse(BaseModel): + alerts: list[AlertResponse] + total: int + + def derive_risk_level(score: float) -> RiskLevel: if score < 0.3: return RiskLevel.low diff --git a/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc b/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc index 9f40dcf37d75ec77614352dae2b4a913a6d7cc87..31df117b6961d67f5db5e836cee82d19336f48a5 100644 GIT binary patch delta 3273 zcmb7G&2JmW72jFzE|=u;L$a*zkF_jIrsXhplD4*EDUl_)v7tzfl*BdLl-O{FGG%g? znq5YTN`h$ROAcxgY|%?mfJTsmt`*Rpa%ywSKaf2H0di^2L4m|>o4z+IQH+~@?P7m3 z^STrITNGwWq8rvi^zAxuT=ml!9QJQ4wl0s87{Xor+v5~EDnqlepK0Do-fPo2v z6z$)aS&R}ouuuNiwxrQPmZ9W^M2A?fXouMu(T*@#w4+C@!BjdnBq6P2&~ZBPK;0wx z$(ESjWa;kdON5>QGuGG&UM4dKIv)W;d=y~}VH{xs!9+NNFzJ6486{u%{mS^n0XdKk2**E}l><3< zDNvd1@pt$%N>2M5%Fopc(EY*>l=pvpkO=MPfD$A;ZrQF?^vaISpGNUZ2$vC_L3kG7 zIfU;ZJOz-|_$ln41_+cb*Rcb&;#kz>3I7vy;_47GzlY4|Jy@~RaRlk;M{v6EX1!dY z1q(w|;9*GEn15NDBlCxjMu{doF#S)WcgT&y%ld~T(TD27#Vh_-u_==C$K%8I=7E}x z@kKPNhxW>pzk;&3l6<;sm%Xx8DIB>HC`HG%`E?=B-Uk25_9lNG<-=%UPdI55#lH)k zyymy#YpovvGfVh_Xo4tZC97WX!f1)OVaS4Hs0hm|d5j0Csemi zHRsG1b~9FvLry4EFexAy|fdMt56%R?f1m$~&%CC{y#+hPnFM$_grVQPE6~ zl{?;amS@iYE9p9tv^y^?uP>Xss=2VbWbzka)9wB_ZXP`w%PY%^ubNiw!bQ`|!E_57 zEfeR(p;dM2wuikc7p)%`FbqR^sOnW&pA>KMzen*b|9{ z&jR@a6uStpJ5hn06eb-vcO4^Dap$1})S^?Z&5IO#lEMFN(r$HS?eBtNn7<#IK=mBY zs$|oFX4PuUru+_0{VKAQEvIaA9mlWXST8XTer~05lJEHy8jI0lfq zYvpq2dj!aN*?XJMp}{Beb)qY9B}r;YZVjZAm_l?pPK*xf(2oNJsi`CEmt`WW`eQxT zOGdxeBcMZMqCNS3sl;_Mctkzn#|1?u@xNwLyb6=bMv+`455QPbYredKulm z1@I`nfThp09nUPmHmCeHvR*>Kx-51aR2qy~Rz3M8{B>kv$>5^!slv7p6M+j085XxE zqZ8X?UNqcRfADV@GsN+?jK{+rkGPFW!V6J)SNvAerX7c}NiL&+D6A3ets`{yN|$2> zGO&_`8vu7!=xj7}7K>YG2Y)qx-hU<4H}!2Ur^3WnI7)6G`R}H#lI_Dkrnbp{@_RKi zMV|59%$$ao6@JtIeP*L|vXK7&Zy~*4BZxrof{4q)QlB^AhcOQKmbc1mr|U!%alX!R z;@`*N2EsfZC%C2HMi5t(?)4)LcWMxYljiI&8t(3Ii%7CwqmWe#Va52=|Ey;*Q%BR} zW_BiYWX*rJcMmQKXcccv`KZf_IXpq)x~6fb4wsVf+~O7rl$zsu9B($9Bdj9eCgu)6 zpw=wN0el+?I91oa66a4`mGtXXht?}>9L%$XAGwO38hZ)EH1^ sbyG2jKM+jPxB4#H2?qr delta 1544 zcmb7D&2Jk;6yI5U{cXpIo%kd1M;nu{4NcP0iWW7YC2c961&T;(A*_{WoNl$BL4p7cPo2Crg4qOozmTMyUa8Z=Lw$NVLWx9w<~;+ zXGE1}MXnd+IX<;-wC6?T&H~Si9G~Vh2N7Nn*`Cgee0D$jfVFGL*7+Q=r2}0gIpgI6 z)?>UP8W_po)r!X1>l#Lz=L`D=o_M2)M~?B@_=s{Wh&(#fc|(OfuK4EP_!;Dn@#BhL zjEfEdL#b}eTh;4H4qB)m$dghfT|*c`4=08-MPvRyl{EvNG=l`z`%To+ze{^E1Y z&vtjm>1aO`9 zdKe90AVQ;OIlhp;On*PTjCQz)@cWc%e$(}WX3rbA94-;{8UZPVj}S00)zLH{L8f;I z-X$RMW7USbO1Z)NWK}9i**~QucSI0JA*h4P-)5HCwyYKYs7^A1kLi%gLN#Mco-Vq> zNkV28baGHtjnL{%g6AccCSr;}P0!o3QZ0&)lche5<4iIRbKL;|4s6E{tYF})-k<`U z^|21sjhyBC_Kw@JT+6xd2OuxZo@cj4yR)CM|I)!qEp|dKl{R8Dxv(Ma(v2g+=Ki0s zJjMtkSj;fui$J}Sb_N?LpaWvp7WZI{ZdoU{nAPtK?CpDGQFYj))mxf&^I1uf|5_5b zNfF|f<9PQ3hck5Wd40_e`kY2|T)1!Kx8<$;r|4|5t7dDQ%T@WL{9xFQ`_NtO+xGa; z#=si{0^k${ruKx4sMZwVjr2UCT&%B5*O}zeBg*ph9= ?") + params.append(created_after.isoformat()) + + if created_before is not None: + conditions.append("a.created_at <= ?") + params.append(created_before.isoformat()) + + where = ("WHERE " + " AND ".join(conditions)) if conditions else "" + query = f""" + SELECT a.*, t.id AS t_id, t.amount, t.merchant_name, t.merchant_category, + t.location, t.timestamp, t.card_id, t.account_id + FROM alerts a + JOIN transactions t ON a.transaction_id = t.id + {where} + ORDER BY a.created_at DESC + """ + + with db() as conn: + rows = conn.execute(query, params).fetchall() + + reveal_pii = show_pii == "true" + alerts = [] + for row in rows: + tx = TransactionResponse( + id=row["t_id"], + amount=row["amount"], + merchant_name=row["merchant_name"], + merchant_category=row["merchant_category"], + location=row["location"], + timestamp=row["timestamp"], + card_id=row["card_id"], + account_id=row["account_id"], + ) + if not reveal_pii: + tx = mask_transaction(tx) + history = [StatusHistoryEntry(**e) for e in json.loads(row["status_history"])] + alerts.append(AlertResponse( + id=row["id"], + transaction_id=row["transaction_id"], + transaction=tx, + risk_score=row["risk_score"], + risk_level=row["risk_level"], + status=row["status"], + analyst_id=row["analyst_id"], + contains_pii=bool(row["contains_pii"]), + created_at=row["created_at"], + updated_at=row["updated_at"], + status_history=history, + )) + + return AlertListResponse(alerts=alerts, total=len(alerts)) + + @router.get("/{alert_id}", response_model=AlertResponse) def get_alert(alert_id: str, show_pii: Literal["true", "false"] | None = Query(default=None)): with db() as conn: diff --git a/fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3f311e0bef906874e149fb216fd73699c738be7 GIT binary patch literal 14213 zcmd5@TWlQHdES|wot?dsk|y}Yy!?Eo!W}D{O8O$Gw1ws&VN5YyOl~YgWn5({b#LTJd(-$l5XNZ3pb~5IfGgz zBQip3XJ#}m^|`E`(K2(!Oioih=8VZb`I)@XMOGLhCrptSmMC1$&sd@;N^fLl3gV=w zi0T{KOi`Q?gJKhEC2?A85kshz#a6KmPgTTyVmr#J*dcbJ91!=52T%@*2gNRwo5Vw6 z80BX1u-J`qi+DsligHLiCibA*DjpYmQEn48u@B{a;!|Qj%I#uA96-54jEXUoJH@z| zKzYA7C?-)pAP$McC?6C@#8H&H#4+&%%7?_0;yB7-F(po*d{}7DWol2|q_vu!C7Aik zbEkThv$o?7KWoe8g+|BqOLp6Cy0Y78HJx7N%!P(*cU{-sKRkV=H#9LeF*!1Z|Kn%J z$DSCY|1P>8J^wUu@aD5(^vlGC}T*St=GOqUf znukm1Z|TeWjSLnbJKd{{y0X!68cmwWSTTOeEUT_%J&$rsHEnA)01M^q> z!MVmIyB_<5A^L1ugPoe(g5pD5`2E|-W>(c0Qay+33@&FofLbI^^K_wK2lZo#G+rWO z`c{6FadGhZ)!0nD39{u}syofDv}Fmy)O1-vp;jv^2dKV{z)W^*G*=NEaxB>fHredQ;W>BJt*|E$Zj4F!GX6s2OUlX-VY61a=ch zFF5aBizs~<-Hi9JCLcjP!9w&}9v83j--EeaFwiya>C8)&CC$~m%r$LEcXcmA)UqRU zP1iE77+z)>UvWcQ%6i$0#*!h7rJQGYxl8(z>6y+8Zf@BWInO+=36t8->o*LMck|2E zjm%=kIfDLzhxg@i)QVK=Xy~W9aYN%WcS94__3XCHQeG59(aX0q)GfD!-m+)i0FJLi zx*M4#1DA>Uv&)qmnWchP5TzZNrJ~S)OVKldQ^_ki-^F``Qwcbgshx1*nTo-TCT6T$ z*D*_3RKj=mjx|nGCn;8CWF~Ggdv*t766hZ@~GNV8#K=xQW_% z6*fn!@Tcf6xK*seUl1Nt!zyfMoWfNAPJE9(vE{l!D@c1xd{TG4t3AKN>BWuoR3UkfV3Q^Qmnt2qZ04c41o zVHcN%r&PzGn&Vs2cIF{K?Uk&b#j^w^2pqaq1*bri@fb(&C>oFWIk)RJ+L&!K4Lx3S z!oenM4%iXTj1!nlynR@`eHc1~grif_dSmoD-ciV+(nof0fPSryDNoPC%yIDT`~H$=m2#2+4u-%P|GJ`}$>5`QchQ{iz)t%#a!d@L=e9_OWOw`e_iqwAe+ zp!OUrmb)N34tm;q@p;Z5Kv}c2L9M9$Cd!N2?`iVOXiSJSG7}?__5?&4#1(`Z#1(9U zix6i*hv0%B2BR9D;XLi?B+fhojA~FjGiuH?mUAqw9z(zB;eELewLI0ps5!3dU{Gp< z2oolYs~nk;UXB^mbS?B2Jg{*ju5=Pvm|weCT+%)L8t!H@B;u~?OL+*(Jg~An%Xt@X z5>^(lDp7k1t9kS*tQHu9)vzKlLfGYjU75v)Nd~QFGIoUkyFwZ}U}!yW~m|f zU`^%Y1Zo6`*yN`Oj1nM@l4O{z=<+Dl#{s~xXV&1@H6#vMW5uPtNhX$0(11@8I8LLM zuEwG zWxJFm6%5s@?hIDMhPu--ZWeufC-iGsjp*DUwM=vig_sit)Dx5*=*o=f>P6RF&O_2wP$!7F+hKy={RVcv8vjsd>92U_>)n-XuCzq&z`2+Aw*Tz!3sW zzD&xcz7}9}k8LMKd_+$%;!9H7N=eD(ta}-H&AmL~>y2?-6Y>l6+7}5h)qRO-zeeCW z0x4R1kZDah0I>h+feH#fWeRUNWLaAfgH`&c58}G0^)z`7%?XlA2*gh!IgnMHq%pXRyq@`%{z_CqpJT`63>>4RVuT!*jJj zMtz;$sS{`rxFfnzGU{V=6Gw~$Uy^vBQ3K*>qAx)_1vEtRYWKSGib5%GK@Rl^2!R!D zfn?GJHhVb>$poiCm}EGMe8O$`B57RcPI4Oxq1%8qY&OV*N^%OIC)p_|!Hf@`f;@Vm zGh>n|atcHRQmKk-fM*;`%=qTU&&VXQNlB#-|L$+;_n#+MG9%|L}zk~LKb^dka=UZ@`}^cL38kE9y|N`yuxvDnwh-{1HdL)(pXV=oMWj7(1S z>&K4u9t>qyKSVMWhGHfs?uZOIY_d{}Q5^+HZbMnhm*}~a>f4c0eSQ2$Iwd66U?90b zOKtQeBv*o#4%`hbC2cDXc|^3cnr(HKcQS2TrDb+t~2J zD6|Dzn?0@@8Z?-)#SPYEfxQjQLH4!bWq*=+AATQO*|4sK`9{Wr#MqH}n(X&HtYNZ( zp^v=$A_t_LHe0`zS76PC_Q(K&&JwV!my7IaK9A>0BwyqVn?(}av+X;TzsQSpjw zjhEaqo~U@Gn9j2h{9;cbG4~(d=G^Z_bN}$}=4P6y2KIQdDQ(2|MZMvE_~ReZZ{_5z z$SjXK&m+1$mDn;0XHyK7OyZA*0A{fjFPY@Ji_DJ9JNhg7bBJHD)z5MR5hVSzuX>5ZjmEP=~`<0@Wr0+sM(b_sA;ptm~l7*?ypQLcj&J8B68 zvg9Ntpg`8aL~scaGkE|#E7iVq^bI=X@N-VvM%-sj1)XwgBY(D5j_K={G4_nrpp~q< zPTFdG4ZX_cAa5fcv6)m}Z%Xt&u-@u;7{Z}FA(z9_in^s&wIO{o_U5W4uc0Hcl>=yq zxbJ)S$bEr#o8&$?8q9r&xdhy21{?q_?U~>{ill_xM;>p1+9U2GO`hgHX!(HqP>XS2 zfw?c#^@{uAzHlDFX+%~a0W48P$l3BraDU2P+4(ExKm?RA{~)!~{9;qY_3sepaUhsn z7L3C4Zi->{GkSvT5BUf51l0&Lu0w)w2}vL}bBw1#2b5lgsH0DvV*;nU4Y(wp;Ztv#yK0YJ9}*|m9Nmdk^mEv6V>>ZoK(38@H<2VQZ6FSOrjV=(30a8WxNMY zBb)E6OryvH--+C{Y#_E}ZE zs$f@Kz2?C-1&nTn?qm?;V)Gy|D={fftu}I2X4b)gSp{viP2fBM4n*t=*)|#S+S7N& z?#{tfaf!IXp|}LawGPUcY4!$zJMtGFVfKe&KG%Zj7zVZ)9UP9(2{U`c%NW(!20%uh zav8~Kj7+_6qbos+31feHHCl|De5-|3S9csauo{bcg^nZ)!GQKC(7KuA}5vDzTmo0KB$M4KZpm^A-PQu#c z)1UND@Nyu?RCtv9#E5v_OcM`9CL&Aa?Kttoi3Z2uIFW(nANdE0jF@l`t4DM`)vttF zB*I{uWGUi+;3bv$z@IRY(2(=;@G3%*vM3n|UWFyBz^(}`d&J+p0_++((!Lgm89LJD z_DHzFE)k{3z99PtQ3_ZOuQ1u62I~RwGxjRrQ{Y4>JnU9>P2#W{b14xQpIg~91wKw+ zZ*KBF)ZBl3cXKoG;S`D@8El{^Q-z$s-r%03`nq=;rG_ zO$=SDrWAe5TqhsJ&#@ivn@asFvAU+(XY(quvHb08Vf^P9*?9nfSfL1M8bV9Aw4DgX zvH4EAKOs?xAws73=!dugx&FU9MrHCSO!ot-f%@xoah@EgmYW}dqt`#YMCgi5x0Cox zd5XYk0x1XV8ET8Xu$8>p%=HfxQ=9Y{NJMXwkF>} zWz9X2Z=tt_Z{HaT;=cNvvM;;RMw;E*cxL@im*1sFew#oFS0!jeT>FDZ$DP3UpXt5t z5qKBilW`pmaSb__34RLOHPRR%Cc7t$_as&~F-08de~plS@3xSBIE{4R5^WIHDg`23 z=Md|_1BlVaFC(i&bQPX^gUcd=-y=TjAM2eg|@E;h< zu>h2z^13E}gqs8>QEw=TouA`owcSNB2_3RgFcZcFObX+DN(JBpH|#V5CD>_%Pdo#@ z5o}iHIqV7oC&!sG6lD zGH=vE^yF>q)}+#%Va$?)X|sHXpc-;8>nU#Jo%=7$5bfMfq$A27qb0F(bn;HScZMF_ z!-i#$UAcL!>`FJQk~o4qOC)h%7fm`g4T4?SDA;Ad+s(O@Gz5Noi*hfG=*<1MTx*&0 zCb7GeKEy#UQFe>VCFbMLp}$NAtMUt|Rj7vDg)taC@ghCpVlU;(^aR!LL?t#xI~wDa z_!wNrQ7xD7&_(rn4o9|@7@+W1ka|=_?*JL45v7oSjl2P-795~HiX~&KZ}zpJKl;qS z_uf0nnMjG{)$vRiEaos&O;hF}Gviu;sQ;yr*~mM^+m3X`g|_R|ovx&aHF7ON8I06B zWPLYP_Q;xs-`Sex=aG4WjBuOf69Q1DDf1~NpOB5HVo|6Bu_z=#aoQq8*Z$$)^(lG{ z5n}|W{9=>{k2FGsTP+vMWn>Y@k*_iOpeG3&2dE9oDXMQFu${nF0^cIgmwoTk?I{AE zZ0Uq)3BN**_od)Bs%!8MpH;)(5_cU5ixWvEzDhDr`O0T7?)gEEamI(a&`i!xNy`R?i&Dnt?p>mYt$ za2cQRjg6l%u8wCFJ-Fq;wb{B2_zjT zxlC;-Il+G}Q;*XF5~mq6{N>96d~nEFBvjYoKrPr#HBA?ivPyo2aiIx@R6!9d+znH) z+qc5hYqw4b8bo(X&a>Lp0V~0wxlD(|} literal 0 HcmV?d00001 diff --git a/fraud-alert-service/tests/test_filtering.py b/fraud-alert-service/tests/test_filtering.py new file mode 100644 index 00000000..0ca24d36 --- /dev/null +++ b/fraud-alert-service/tests/test_filtering.py @@ -0,0 +1,276 @@ +import time + +import pytest + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +TX_BASE = { + "amount": 150.00, + "merchant_name": "Test Merchant", + "merchant_category": "electronics", + "location": "Charlotte, NC", + "timestamp": "2024-04-01T10:00:00Z", + "card_id": "4111111111111234", + "account_id": "ACC0000000001", +} + + +def make_transaction(client, **overrides): + return client.post("/transactions", json={**TX_BASE, **overrides}).json() + + +def make_alert(client, transaction_id, risk_score=0.5): + return client.post("/alerts", json={"transaction_id": transaction_id, "risk_score": risk_score}).json() + + +def assign(client, alert_id, analyst_id="analyst-1"): + return client.patch(f"/alerts/{alert_id}/assign", json={"analyst_id": analyst_id}).json() + + +def transition(client, alert_id, status, changed_by="system"): + return client.patch(f"/alerts/{alert_id}/status", json={"status": status, "changed_by": changed_by}).json() + + +# --------------------------------------------------------------------------- +# No filters — returns all alerts +# --------------------------------------------------------------------------- + +def test_list_alerts_no_filters_returns_all(client): + for _ in range(3): + tx = make_transaction(client) + make_alert(client, tx["id"]) + response = client.get("/alerts") + assert response.status_code == 200 + data = response.json() + assert data["total"] == 3 + assert len(data["alerts"]) == 3 + + +def test_list_alerts_empty_returns_200(client): + response = client.get("/alerts") + assert response.status_code == 200 + assert response.json() == {"alerts": [], "total": 0} + + +# --------------------------------------------------------------------------- +# Single filters +# --------------------------------------------------------------------------- + +def test_filter_by_status_pending(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + a2 = make_alert(client, tx2["id"]) + # Move a2 to under_review + assign(client, a2["id"]) + transition(client, a2["id"], "under_review") + + data = client.get("/alerts?status=pending").json() + assert data["total"] == 1 + assert data["alerts"][0]["id"] == a1["id"] + + +def test_filter_by_status_under_review(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + make_alert(client, tx1["id"]) + a2 = make_alert(client, tx2["id"]) + assign(client, a2["id"]) + transition(client, a2["id"], "under_review") + + data = client.get("/alerts?status=under_review").json() + assert data["total"] == 1 + assert data["alerts"][0]["id"] == a2["id"] + + +def test_filter_by_risk_level_critical(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + make_alert(client, tx1["id"], risk_score=0.9) # critical + make_alert(client, tx2["id"], risk_score=0.3) # medium + + data = client.get("/alerts?risk_level=critical").json() + assert data["total"] == 1 + assert data["alerts"][0]["risk_level"] == "critical" + + +def test_filter_by_analyst_id(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + a2 = make_alert(client, tx2["id"]) + assign(client, a1["id"], "analyst-42") + assign(client, a2["id"], "analyst-99") + + data = client.get("/alerts?analyst_id=analyst-42").json() + assert data["total"] == 1 + assert data["alerts"][0]["analyst_id"] == "analyst-42" + + +def test_filter_by_analyst_id_unassigned(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + a2 = make_alert(client, tx2["id"]) + assign(client, a2["id"], "analyst-1") + + data = client.get("/alerts?analyst_id=unassigned").json() + assert data["total"] == 1 + assert data["alerts"][0]["id"] == a1["id"] + + +def test_filter_by_created_after(client): + tx1 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + after_ts = a1["created_at"] + + time.sleep(0.05) + + tx2 = make_transaction(client) + a2 = make_alert(client, tx2["id"]) + + data = client.get(f"/alerts?created_after={after_ts}").json() + ids = [a["id"] for a in data["alerts"]] + assert a2["id"] in ids + assert a1["id"] in ids # created_at >= boundary is inclusive + + +def test_filter_by_created_before(client): + tx1 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + before_ts = a1["created_at"] + + time.sleep(0.05) + + tx2 = make_transaction(client) + make_alert(client, tx2["id"]) + + data = client.get(f"/alerts?created_before={before_ts}").json() + ids = [a["id"] for a in data["alerts"]] + assert a1["id"] in ids + assert len(ids) == 1 + + +# --------------------------------------------------------------------------- +# Combined filters +# --------------------------------------------------------------------------- + +def test_combined_status_and_risk_level(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + tx3 = make_transaction(client) + a1 = make_alert(client, tx1["id"], risk_score=0.9) # critical, pending + a2 = make_alert(client, tx2["id"], risk_score=0.9) # critical, under_review + make_alert(client, tx3["id"], risk_score=0.3) # medium, pending + + assign(client, a2["id"]) + transition(client, a2["id"], "under_review") + + data = client.get("/alerts?status=pending&risk_level=critical").json() + assert data["total"] == 1 + assert data["alerts"][0]["id"] == a1["id"] + + +def test_combined_status_and_analyst_id(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + a2 = make_alert(client, tx2["id"]) + assign(client, a1["id"], "analyst-1") + assign(client, a2["id"], "analyst-1") + transition(client, a1["id"], "under_review") + + data = client.get("/alerts?status=under_review&analyst_id=analyst-1").json() + assert data["total"] == 1 + assert data["alerts"][0]["id"] == a1["id"] + + +def test_combined_date_range(client): + tx1 = make_transaction(client) + a1 = make_alert(client, tx1["id"]) + time.sleep(0.05) + tx2 = make_transaction(client) + a2 = make_alert(client, tx2["id"]) + time.sleep(0.05) + tx3 = make_transaction(client) + make_alert(client, tx3["id"]) + + after = a1["created_at"] + before = a2["created_at"] + data = client.get(f"/alerts?created_after={after}&created_before={before}").json() + ids = [a["id"] for a in data["alerts"]] + assert a1["id"] in ids + assert a2["id"] in ids + + +# --------------------------------------------------------------------------- +# Edge cases +# --------------------------------------------------------------------------- + +def test_invalid_status_returns_422(client): + response = client.get("/alerts?status=not_a_status") + assert response.status_code == 422 + + +def test_invalid_risk_level_returns_422(client): + response = client.get("/alerts?risk_level=extreme") + assert response.status_code == 422 + + +def test_invalid_created_after_returns_422(client): + response = client.get("/alerts?created_after=not-a-date") + assert response.status_code == 422 + + +def test_invalid_created_before_returns_422(client): + response = client.get("/alerts?created_before=not-a-date") + assert response.status_code == 422 + + +def test_filters_matching_zero_returns_empty(client): + tx = make_transaction(client) + make_alert(client, tx["id"]) + data = client.get("/alerts?status=confirmed_fraud").json() + assert data == {"alerts": [], "total": 0} + + +def test_created_after_greater_than_before_returns_empty(client): + tx = make_transaction(client) + a = make_alert(client, tx["id"]) + data = client.get(f"/alerts?created_after={a['created_at']}&created_before=2020-01-01T00:00:00Z").json() + assert data == {"alerts": [], "total": 0} + + +def test_results_sorted_by_created_at_descending(client): + for _ in range(3): + tx = make_transaction(client) + make_alert(client, tx["id"]) + time.sleep(0.02) + + data = client.get("/alerts").json() + timestamps = [a["created_at"] for a in data["alerts"]] + assert timestamps == sorted(timestamps, reverse=True) + + +# --------------------------------------------------------------------------- +# PII masking on list endpoint +# --------------------------------------------------------------------------- + +def test_list_alerts_masks_pii_by_default(client): + tx = make_transaction(client) + make_alert(client, tx["id"]) + data = client.get("/alerts").json() + alert = data["alerts"][0] + assert alert["transaction"]["card_id"] == "****1234" + assert alert["transaction"]["account_id"] == "****0001" + + +def test_list_alerts_show_pii_true_reveals_values(client): + tx = make_transaction(client) + make_alert(client, tx["id"]) + data = client.get("/alerts?show_pii=true").json() + alert = data["alerts"][0] + assert alert["transaction"]["card_id"] == TX_BASE["card_id"] + assert alert["transaction"]["account_id"] == TX_BASE["account_id"] From 9ab5d081d781f2a5ce11fdae0a50a8d24c45fab2 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 18:29:31 -0400 Subject: [PATCH 12/17] Wrote specs for summarization endpoint functionality. Updated the todo. --- SPECS/summary-stats.md | 46 ++++++++++++++++++++++++++++++++++++++++++ TODO.md | 7 +++++++ 2 files changed, 53 insertions(+) create mode 100644 SPECS/summary-stats.md diff --git a/SPECS/summary-stats.md b/SPECS/summary-stats.md new file mode 100644 index 00000000..55c1d723 --- /dev/null +++ b/SPECS/summary-stats.md @@ -0,0 +1,46 @@ +# Feature Spec: Summary Statistics + +## Goal +- Provide an aggregation endpoint that gives analysts and team leads a dashboard-level view of fraud alert volume, distribution, and resolution performance. + +## Scope +- In: GET /alerts/summary endpoint returning counts by status, counts by risk level, and average resolution time +- Out: Historical trend data, per-analyst performance metrics, real-time streaming updates + +## Requirements + +### Response Structure +- `total_alerts` — total number of alerts in the system +- `by_status` — object with counts for each status: `pending`, `under_review`, `confirmed_fraud`, `false_positive`, `escalated` +- `by_risk_level` — object with counts for each risk level: `low`, `medium`, `high`, `critical` +- `avg_resolution_time_seconds` — average time from alert creation to reaching a terminal state (confirmed_fraud, false_positive, or escalated), in seconds. Null if no alerts have been resolved. +- All counts default to 0 if no alerts match that category + +### Resolution Time Calculation +- Resolution time = timestamp of terminal status transition minus `created_at` +- Only alerts in terminal states (confirmed_fraud, false_positive, escalated) are included in the average +- If no alerts are in a terminal state, `avg_resolution_time_seconds` is null (not 0) +- Resolution time is calculated from the `status_history` entries, using the timestamp of the terminal transition + +## Acceptance Criteria + +### Basic Stats +- [ ] GET /alerts/summary returns 200 with the correct response structure +- [ ] total_alerts matches the actual number of alerts +- [ ] by_status counts are accurate for each status category +- [ ] by_risk_level counts are accurate for each risk level category +- [ ] All status keys are present even when count is 0 +- [ ] All risk_level keys are present even when count is 0 + +### Resolution Time +- [ ] avg_resolution_time_seconds is calculated correctly for resolved alerts +- [ ] avg_resolution_time_seconds is null when no alerts have been resolved +- [ ] Resolution time uses the terminal status_history entry timestamp minus created_at +- [ ] Average is computed across all terminal states (confirmed_fraud, false_positive, escalated) + +### Edge Cases +- [ ] Summary with zero alerts returns all counts as 0 and avg_resolution_time as null +- [ ] Summary with one resolved alert returns that alert's resolution time as the average +- [ ] Summary with multiple resolved alerts returns the correct arithmetic mean +- [ ] Alerts in non-terminal states (pending, under_review) do not affect avg_resolution_time +- [ ] by_status and by_risk_level always include all possible keys regardless of data present \ No newline at end of file diff --git a/TODO.md b/TODO.md index e542e804..4fb18463 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,13 @@ ## New Feature Proposals - +## In Progress + +### Summary Stats (SPECS/summary-stats.md) +- [ ] Add `SummaryResponse` Pydantic model to `src/models.py` +- [ ] Implement `GET /alerts/summary` in `src/routes/alerts.py` +- [ ] Write `tests/test_summary_stats.py` covering all acceptance criteria + From 1fb007b5ea5c9275322846504edb1f08bdd31401 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 19:16:55 -0400 Subject: [PATCH 13/17] Implemented the last spec, summary stats, and wrote and executed tests. I also added a new README.md to explain the project and how to run/test it. Made final changes to tests to ensure that they are specific and understandable. Modified the git ignore so that the db I was testing with won't be included in the PR. I also tested via the docs thoroughly to make sure everything works well. --- .gitignore | 4 + README.md | 43 ++++ SPECS/alerts.md | 38 ++-- SPECS/filtering.md | 32 +-- SPECS/pii-masking.md | 32 +-- SPECS/state-machine.md | 48 ++--- SPECS/summary-stats.md | 30 +-- SPECS/transactions.md | 20 +- TODO.md | 6 - fraud-alert-service/fraud_alerts.db | Bin 352256 -> 368640 bytes fraud-alert-service/pytest.ini | 2 + .../src/__pycache__/models.cpython-310.pyc | Bin 4456 -> 5058 bytes fraud-alert-service/src/models.py | 32 ++- .../routes/__pycache__/alerts.cpython-310.pyc | Bin 6707 -> 7966 bytes fraud-alert-service/src/routes/alerts.py | 36 ++++ .../test_alerts.cpython-310-pytest-8.3.3.pyc | Bin 11715 -> 11829 bytes ...est_filtering.cpython-310-pytest-8.3.3.pyc | Bin 14213 -> 14198 bytes ...state_machine.cpython-310-pytest-8.3.3.pyc | Bin 12030 -> 12025 bytes ..._transactions.cpython-310-pytest-8.3.3.pyc | Bin 7792 -> 8016 bytes fraud-alert-service/tests/test_alerts.py | 8 +- fraud-alert-service/tests/test_filtering.py | 2 +- .../tests/test_state_machine.py | 3 +- .../tests/test_summary_stats.py | 204 ++++++++++++++++++ .../tests/test_transactions.py | 6 +- 24 files changed, 432 insertions(+), 114 deletions(-) create mode 100644 README.md create mode 100644 fraud-alert-service/pytest.ini create mode 100644 fraud-alert-service/tests/test_summary_stats.py diff --git a/.gitignore b/.gitignore index e69de29b..7882cba6 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,4 @@ +fraud-alert-service/fraud_alerts.db +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..75a0f838 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Fraud Alert Validation Service + +This project is essentially a REST API for storing fraud alerts, enforcing lifecycle transitions, and properly handling customer data to comply with Ally's PII sensitivity principles. + +I put everything in fraud-alert-service to keep the project separated from given spec-driven-dev environment. + +To test this, just follow the steps below and test out the interactive docs at `http://localhost:8000/docs`. + +## Setup + +```bash +cd fraud-alert-service +pip install -r requirements.txt +``` + +## Run + +```bash +uvicorn src.main:app --reload +``` + +The API will be available at `http://localhost:8000`. Interactive docs at `http://localhost:8000/docs`. + +## Test + +```bash +python -m pytest tests/ -v +``` + +## API Overview + +- POST | `/transactions` | Create a transaction +- GET | `/transactions/{id}` | Get a transaction by ID +- POST | `/alerts` | Create an alert for a transaction +- GET | `/alerts` | List and filter alerts +- GET | `/alerts/summary` | Aggregated stats by status, risk level, and resolution time +- GET | `/alerts/{id}` | Get a single alert +- PATCH | `/alerts/{id}/assign` | Assign an analyst to an alert +- PATCH | `/alerts/{id}/status` | Transition alert status + +## PII Masking + +`card_id` and `account_id` are masked by default in all responses (e.g. `****1234`). Append `?show_pii=true` to any endpoint to reveal full values. diff --git a/SPECS/alerts.md b/SPECS/alerts.md index b0af4df6..4b6305ec 100644 --- a/SPECS/alerts.md +++ b/SPECS/alerts.md @@ -27,22 +27,22 @@ - GET response returns the alert with its linked transaction's PII fields masked by default ## Acceptance Criteria -- [ ] POST /alerts creates an alert and returns it with generated UUID, pending status, and derived risk_level -- [ ] POST /alerts returns 422 for missing transaction_id or risk_score -- [ ] POST /alerts returns 404 if transaction_id does not reference an existing transaction -- [ ] POST /alerts returns 409 if an alert already exists for the given transaction_id -- [ ] POST /alerts returns 422 for risk_score < 0.0 -- [ ] POST /alerts returns 422 for risk_score > 1.0 -- [ ] POST /alerts returns 422 for non-numeric risk_score -- [ ] Risk level is correctly derived at boundary: score 0.0 → low -- [ ] Risk level is correctly derived at boundary: score 0.29 → low -- [ ] Risk level is correctly derived at boundary: score 0.3 → medium -- [ ] Risk level is correctly derived at boundary: score 0.59 → medium -- [ ] Risk level is correctly derived at boundary: score 0.6 → high -- [ ] Risk level is correctly derived at boundary: score 0.79 → high -- [ ] Risk level is correctly derived at boundary: score 0.8 → critical -- [ ] Risk level is correctly derived at boundary: score 1.0 → critical -- [ ] Alert is created with status "pending" and a single status_history entry -- [ ] GET /alerts/{id} returns the alert with all fields populated -- [ ] GET /alerts/{id} returns 404 for nonexistent alert ID -- [ ] Client cannot override risk_level, status, or created_at on creation \ No newline at end of file +- [x] POST /alerts creates an alert and returns it with generated UUID, pending status, and derived risk_level +- [x] POST /alerts returns 422 for missing transaction_id or risk_score +- [x] POST /alerts returns 404 if transaction_id does not reference an existing transaction +- [x] POST /alerts returns 409 if an alert already exists for the given transaction_id +- [x] POST /alerts returns 422 for risk_score < 0.0 +- [x] POST /alerts returns 422 for risk_score > 1.0 +- [x] POST /alerts returns 422 for non-numeric risk_score +- [x] Risk level is correctly derived at boundary: score 0.0 → low +- [x] Risk level is correctly derived at boundary: score 0.29 → low +- [x] Risk level is correctly derived at boundary: score 0.3 → medium +- [x] Risk level is correctly derived at boundary: score 0.59 → medium +- [x] Risk level is correctly derived at boundary: score 0.6 → high +- [x] Risk level is correctly derived at boundary: score 0.79 → high +- [x] Risk level is correctly derived at boundary: score 0.8 → critical +- [x] Risk level is correctly derived at boundary: score 1.0 → critical +- [x] Alert is created with status "pending" and a single status_history entry +- [x] GET /alerts/{id} returns the alert with all fields populated +- [x] GET /alerts/{id} returns 404 for nonexistent alert ID +- [x] Client cannot override risk_level, status, or created_at on creation \ No newline at end of file diff --git a/SPECS/filtering.md b/SPECS/filtering.md index 884b39d7..4a3af92a 100644 --- a/SPECS/filtering.md +++ b/SPECS/filtering.md @@ -26,23 +26,23 @@ ## Acceptance Criteria ### Single Filters -- [ ] GET /alerts with no filters returns all alerts -- [ ] GET /alerts?status=pending returns only pending alerts -- [ ] GET /alerts?risk_level=critical returns only critical alerts -- [ ] GET /alerts?analyst_id=analyst_1 returns only alerts assigned to analyst_1 -- [ ] GET /alerts?analyst_id=unassigned returns only unassigned alerts -- [ ] GET /alerts?created_after= returns alerts created on or after that time -- [ ] GET /alerts?created_before= returns alerts created on or before that time +- [x] GET /alerts with no filters returns all alerts +- [x] GET /alerts?status=pending returns only pending alerts +- [x] GET /alerts?risk_level=critical returns only critical alerts +- [x] GET /alerts?analyst_id=analyst_1 returns only alerts assigned to analyst_1 +- [x] GET /alerts?analyst_id=unassigned returns only unassigned alerts +- [x] GET /alerts?created_after= returns alerts created on or after that time +- [x] GET /alerts?created_before= returns alerts created on or before that time ### Combined Filters -- [ ] GET /alerts?status=pending&risk_level=high returns alerts matching both conditions -- [ ] GET /alerts?status=under_review&analyst_id=analyst_1 returns correct intersection -- [ ] GET /alerts?created_after=&created_before= returns alerts within the date range +- [x] GET /alerts?status=pending&risk_level=high returns alerts matching both conditions +- [x] GET /alerts?status=under_review&analyst_id=analyst_1 returns correct intersection +- [x] GET /alerts?created_after=&created_before= returns alerts within the date range ### Edge Cases -- [ ] Invalid status value returns 422 -- [ ] Invalid risk_level value returns 422 -- [ ] Invalid datetime format for created_after or created_before returns 422 -- [ ] Filters that match zero alerts return {"alerts": [], "total": 0} with 200 status -- [ ] Date range where created_after > created_before returns empty results (not an error) -- [ ] Results are sorted by created_at descending by default \ No newline at end of file +- [x] Invalid status value returns 422 +- [x] Invalid risk_level value returns 422 +- [x] Invalid datetime format for created_after or created_before returns 422 +- [x] Filters that match zero alerts return {"alerts": [], "total": 0} with 200 status +- [x] Date range where created_after > created_before returns empty results (not an error) +- [x] Results are sorted by created_at descending by default \ No newline at end of file diff --git a/SPECS/pii-masking.md b/SPECS/pii-masking.md index f9c94409..82d3d1b5 100644 --- a/SPECS/pii-masking.md +++ b/SPECS/pii-masking.md @@ -37,25 +37,25 @@ ## Acceptance Criteria ### Default Masking -- [ ] GET /transactions/{id} returns card_id and account_id masked (e.g., "****5678") -- [ ] POST /transactions response body returns masked PII fields -- [ ] GET /alerts/{id} returns embedded transaction data with masked PII -- [ ] GET /alerts list returns all embedded transaction data with masked PII -- [ ] Masking shows last 4 characters: "1234567890" → "****7890" -- [ ] Values with 4 or fewer characters are fully masked: "1234" → "****" +- [x] GET /transactions/{id} returns card_id and account_id masked (e.g., "****5678") +- [x] POST /transactions response body returns masked PII fields +- [x] GET /alerts/{id} returns embedded transaction data with masked PII +- [x] GET /alerts list returns all embedded transaction data with masked PII +- [x] Masking shows last 4 characters: "1234567890" → "****7890" +- [x] Values with 4 or fewer characters are fully masked: "1234" → "****" ### Authorized Access -- [ ] GET /transactions/{id}?show_pii=true returns full card_id and account_id -- [ ] GET /alerts/{id}?show_pii=true returns full PII in embedded transaction -- [ ] GET /alerts?show_pii=true returns full PII across all results -- [ ] show_pii=false behaves the same as omitting the parameter (masked) +- [x] GET /transactions/{id}?show_pii=true returns full card_id and account_id +- [x] GET /alerts/{id}?show_pii=true returns full PII in embedded transaction +- [x] GET /alerts?show_pii=true returns full PII across all results +- [x] show_pii=false behaves the same as omitting the parameter (masked) ### Consistency -- [ ] PII masking is applied consistently across all endpoints — no endpoint leaks unmasked data by default -- [ ] Masking does not affect stored data — full values are preserved in the database -- [ ] The contains_pii flag on alerts is set to true by default +- [x] PII masking is applied consistently across all endpoints — no endpoint leaks unmasked data by default +- [x] Masking does not affect stored data — full values are preserved in the database +- [x] The contains_pii flag on alerts is set to true by default ### Edge Cases -- [ ] Empty string card_id or account_id is masked as "****" -- [ ] Very long PII values are correctly masked (only last 4 shown) -- [ ] show_pii parameter with non-boolean values (e.g., "yes", "1") is handled gracefully (treat as false or return 422) \ No newline at end of file +- [x] Empty string card_id or account_id is masked as "****" +- [x] Very long PII values are correctly masked (only last 4 shown) +- [x] show_pii parameter with non-boolean values (e.g., "yes", "1") is handled gracefully (treat as false or return 422) \ No newline at end of file diff --git a/SPECS/state-machine.md b/SPECS/state-machine.md index 2b18b6d7..566469ff 100644 --- a/SPECS/state-machine.md +++ b/SPECS/state-machine.md @@ -41,33 +41,33 @@ ## Acceptance Criteria ### Valid Transitions -- [ ] pending → under_review succeeds when analyst_id is assigned -- [ ] under_review → confirmed_fraud succeeds -- [ ] under_review → false_positive succeeds -- [ ] under_review → escalated succeeds -- [ ] Each successful transition appends to status_history with correct status, timestamp, and changed_by +- [x] pending → under_review succeeds when analyst_id is assigned +- [x] under_review → confirmed_fraud succeeds +- [x] under_review → false_positive succeeds +- [x] under_review → escalated succeeds +- [x] Each successful transition appends to status_history with correct status, timestamp, and changed_by ### Invalid Transitions -- [ ] pending → confirmed_fraud returns 409 -- [ ] pending → false_positive returns 409 -- [ ] pending → escalated returns 409 -- [ ] under_review → pending returns 409 -- [ ] confirmed_fraud → any status returns 409 -- [ ] false_positive → any status returns 409 -- [ ] escalated → any status returns 409 -- [ ] pending → under_review without analyst_id assigned returns 409 (or 422) +- [x] pending → confirmed_fraud returns 409 +- [x] pending → false_positive returns 409 +- [x] pending → escalated returns 409 +- [x] under_review → pending returns 409 +- [x] confirmed_fraud → any status returns 409 +- [x] false_positive → any status returns 409 +- [x] escalated → any status returns 409 +- [x] pending → under_review without analyst_id assigned returns 409 (or 422) ### Analyst Assignment -- [ ] Assigning analyst to a pending alert succeeds -- [ ] Assigning analyst to an under_review alert succeeds (re-assignment) -- [ ] Assigning analyst to a confirmed_fraud alert returns 409 -- [ ] Assigning analyst to a false_positive alert returns 409 -- [ ] Assigning analyst to an escalated alert returns 409 -- [ ] Assignment updates the updated_at timestamp +- [x] Assigning analyst to a pending alert succeeds +- [x] Assigning analyst to an under_review alert succeeds (re-assignment) +- [x] Assigning analyst to a confirmed_fraud alert returns 409 +- [x] Assigning analyst to a false_positive alert returns 409 +- [x] Assigning analyst to an escalated alert returns 409 +- [x] Assignment updates the updated_at timestamp ### Audit Trail -- [ ] A newly created alert has exactly one status_history entry (pending) -- [ ] After transitioning pending → under_review → confirmed_fraud, status_history has 3 entries -- [ ] Status history entries are in chronological order -- [ ] Each entry contains the correct changed_by value from the request -- [ ] Status history is immutable — previous entries are unchanged after new transitions \ No newline at end of file +- [x] A newly created alert has exactly one status_history entry (pending) +- [x] After transitioning pending → under_review → confirmed_fraud, status_history has 3 entries +- [x] Status history entries are in chronological order +- [x] Each entry contains the correct changed_by value from the request +- [x] Status history is immutable — previous entries are unchanged after new transitions \ No newline at end of file diff --git a/SPECS/summary-stats.md b/SPECS/summary-stats.md index 55c1d723..6b6e561e 100644 --- a/SPECS/summary-stats.md +++ b/SPECS/summary-stats.md @@ -25,22 +25,22 @@ ## Acceptance Criteria ### Basic Stats -- [ ] GET /alerts/summary returns 200 with the correct response structure -- [ ] total_alerts matches the actual number of alerts -- [ ] by_status counts are accurate for each status category -- [ ] by_risk_level counts are accurate for each risk level category -- [ ] All status keys are present even when count is 0 -- [ ] All risk_level keys are present even when count is 0 +- [x] GET /alerts/summary returns 200 with the correct response structure +- [x] total_alerts matches the actual number of alerts +- [x] by_status counts are accurate for each status category +- [x] by_risk_level counts are accurate for each risk level category +- [x] All status keys are present even when count is 0 +- [x] All risk_level keys are present even when count is 0 ### Resolution Time -- [ ] avg_resolution_time_seconds is calculated correctly for resolved alerts -- [ ] avg_resolution_time_seconds is null when no alerts have been resolved -- [ ] Resolution time uses the terminal status_history entry timestamp minus created_at -- [ ] Average is computed across all terminal states (confirmed_fraud, false_positive, escalated) +- [x] avg_resolution_time_seconds is calculated correctly for resolved alerts +- [x] avg_resolution_time_seconds is null when no alerts have been resolved +- [x] Resolution time uses the terminal status_history entry timestamp minus created_at +- [x] Average is computed across all terminal states (confirmed_fraud, false_positive, escalated) ### Edge Cases -- [ ] Summary with zero alerts returns all counts as 0 and avg_resolution_time as null -- [ ] Summary with one resolved alert returns that alert's resolution time as the average -- [ ] Summary with multiple resolved alerts returns the correct arithmetic mean -- [ ] Alerts in non-terminal states (pending, under_review) do not affect avg_resolution_time -- [ ] by_status and by_risk_level always include all possible keys regardless of data present \ No newline at end of file +- [x] Summary with zero alerts returns all counts as 0 and avg_resolution_time as null +- [x] Summary with one resolved alert returns that alert's resolution time as the average +- [x] Summary with multiple resolved alerts returns the correct arithmetic mean +- [x] Alerts in non-terminal states (pending, under_review) do not affect avg_resolution_time +- [x] by_status and by_risk_level always include all possible keys regardless of data present \ No newline at end of file diff --git a/SPECS/transactions.md b/SPECS/transactions.md index a27db25b..1d5e8c64 100644 --- a/SPECS/transactions.md +++ b/SPECS/transactions.md @@ -19,13 +19,13 @@ - GET response returns the transaction with PII fields masked by default ## Acceptance Criteria -- [ ] POST /transactions creates a transaction and returns it with a generated UUID -- [ ] POST /transactions returns 422 for missing required fields -- [ ] POST /transactions returns 422 for amount <= 0 -- [ ] POST /transactions returns 422 for invalid merchant_category -- [ ] POST /transactions returns 422 for invalid timestamp format -- [ ] GET /transactions/{id} returns the transaction with PII fields masked -- [ ] GET /transactions/{id} returns 404 for nonexistent ID -- [ ] POST /transactions accepts and stores all valid merchant_category values -- [ ] Response includes server-generated `id` that was not provided in the request body -- [ ] Extra/unknown fields in the request body are ignored or rejected (pick one, document it) \ No newline at end of file +- [x] POST /transactions creates a transaction and returns it with a generated UUID +- [x] POST /transactions returns 422 for missing required fields +- [x] POST /transactions returns 422 for amount <= 0 +- [x] POST /transactions returns 422 for invalid merchant_category +- [x] POST /transactions returns 422 for invalid timestamp format +- [x] GET /transactions/{id} returns the transaction with PII fields masked +- [x] GET /transactions/{id} returns 404 for nonexistent ID +- [x] POST /transactions accepts and stores all valid merchant_category values +- [x] Response includes server-generated `id` that was not provided in the request body +- [x] Extra/unknown fields in the request body are ignored or rejected (pick one, document it) \ No newline at end of file diff --git a/TODO.md b/TODO.md index 4fb18463..125112d1 100644 --- a/TODO.md +++ b/TODO.md @@ -6,12 +6,6 @@ ## New Feature Proposals - -## In Progress - -### Summary Stats (SPECS/summary-stats.md) -- [ ] Add `SummaryResponse` Pydantic model to `src/models.py` -- [ ] Implement `GET /alerts/summary` in `src/routes/alerts.py` -- [ ] Write `tests/test_summary_stats.py` covering all acceptance criteria diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db index cba1adfd04bd73555a1a0f2c4f0dd4e1d19cae0e..fa0c20b7b16a87846947f6cec90575ecf5278843 100644 GIT binary patch delta 9033 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 delta 2447 zcmYk8dt6mj7RS%pkMp|cK|*M#pm1@xggktNI;I!XL^A;aLj}z=D@;E~<|8do93Lrq z>5MKv_V5*xHCPDRQPaE_HRhO^BcosfHEP4;YsPA_Pam6oVdgWPzxLytwb%ac-*4?x z9*isx-W%x2qa;b1T-9eJMD2V0OChh^ts}nr#VM72)Hr)c z=+ivM$bQ(#2#c;S37oL%hs-z?){WUcK}`AWarRler@dI+T_0I;!E)})=yk7>K=WbH ztl0yfNEgQ^;y4)!V#%A&jd=?+dZFYqnVXv^K*$5UP z{Y1Z|4HW4-`VhU3+ypQAn7m4!BjZRP_yJDAE?5E+MdH7DoIAmlj@fkDnV90MA9wtq zsffvG5G^?oXNEhI&OG3}d?sob*{b9#_d~P-@_Bi$Tp>@9Q)CXA{374S*YF}fkZY`s z9bg-z@1&Fe4^NL{!$QcbkNu*UVoWJCVf6yY$C*1Jj=J5>u~SIA&8>@8owd%IVGXtn^K0{v88GLV!_9D`1BV2l4^9oh@Ihp$93Z>p zIr4C+Ne<^78b2-+Kdvj)(&xbIrtK5}F>4A*z}oXf z#sOtw@a`@+LGXGm@w)ydff`?$LgL((^8^56riGO4xGUa>BblIYc(epO7cXZ!MK{ zqch}EIa7||-}6&^7hl3Bszuz(eqvv+J*ebc&Yqh>i&*?mzceHZ&29AOQ&y1#*=zh9heqEWbjJ8)QQEH@e-6~WX%`Ki! z&1cO~W)I_r(P(TlN{q2mquL@}x09he_~~HkF+1c;GpH8hXY05Rm#*W9`14xsb8?pW zFzrvwhX*Qo8Bh#dBFPxNo5Z1~m89XER)MKvEziak2T1~xUX@?Nuu8t1seUOQ_X6~F zPqmN)z?=I?tn2@h^aCNln&*67@OIQQFV1Kcf>_={67iu8JPBh*l34s8)T-IU6J@nH zR1lr_)_TFhYwLL~GlNoZyk`r!4|moPq5P*YtA(VXypO~Z$iSSlBE!AS=SB&x%kHuObBozFLT;TefZT;k|0ZWL-Fx?BzPemp;<&))lA~Crj6uvC3*M} z=G7ogEc+Xpui=hK#ETVe#OM67*yqN)%##40UoVbOm?#sgwz5!>Cf%{&FVGVYg>V&Z z;{CAXoY-K>CZ2++&7w8yLxJ#@3q-*|$4HzE6`f+L3gccGR(7^`%G`@7ZGz3b4dOJu zY9&dS-bTEmb?7dQkv^hO$Ns=xWO*!+N%Xv}cIy0P>v1dHGCOtNRdu=<9mX+ZGfaoU zu$H6}8NQ^GaYDPW;-YpE9YLnr(}WTRc>e6!_l1;wu-JqY=0kPJ&3rEp# zBSa_>-xx&`-2)S8R+TY=UzL>o!Y9J3*sY!B8A_|ZSb0F7sK?6(-9_#6U+h-~t?VbP zW?iMXePvd_~2b6aB zQRC`Gs}^2R>)2@Sx0$+j=#!ZGy&ze(nt9!rd)R|!2u?gM8b$lMh0$!!0y0wb6Sx%Hztu51Xv;@0G{Y7b3o5a<-fQQj<$yi(z zWIm8^TaaC3v_Y_+n#8??IPUfa7E2f#DTdlM@_2r`Q*jO72sef|vLq&75;L`%gaF%) z64{;C$Z#2rq$g4F^ZssCJKaLvlMnG>MvoMA#{d31ru?_q9&kxsfyLXY diff --git a/fraud-alert-service/pytest.ini b/fraud-alert-service/pytest.ini new file mode 100644 index 00000000..2f4c80e3 --- /dev/null +++ b/fraud-alert-service/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto diff --git a/fraud-alert-service/src/__pycache__/models.cpython-310.pyc b/fraud-alert-service/src/__pycache__/models.cpython-310.pyc index c5127541372767afcd8f461a9cea9e4cda60ce35..8cb2f833cbc7e1b890b432284b5775f929a38a81 100644 GIT binary patch delta 1821 zcmah}TW=dh6!zGAx7XRExil9ym$pi+)U6#~(u6?cxFHP+rj1PnF2UM(#;uDJSF@X{ zF%Ll`h^Icl@Ia+Xz+)wZgbkoW}%sc_EPG*NkAN9UU}XXebDIp>@G zed>qlXv;K>0DNEn^i5;F=Y!}NDStVWBIi~Ebd1Ih12o38gHUCho?;VpoLO~=PSD9i zsWM5YN`d(4uUt|H8e!OB3B)-R--k}@(-4)YOckn9jq22Rq&5f*KL}PNCNqUb76VL$ z-h^I*KH44kK(E8t>W+K6eIN7&jT{gbrYD$8`3|%NN`wwUC?~z%GPide zySDE(S~~^KY?paTy~Vd0)Kl4gd;4ya-Mac)yZ?*twwBu3GIQLC<=s7w(YMXQZJRe+ zuFK9(6btSC+2m{{k<2ENsd6f_kWMZnla-22-o2ongC2vZ2B0mSE0n@o#od4^mNCHZ5L z5bx+GXGUOZ5X4CW7=a*x7W5G{ze#@v|5l!8Jz_}tWaa-4D#mp_0Gn~#mJcDw2zUU$ zh>#M0DO2KCRf^?c==1>?0gW7Q5yI2r({R5qHC^0PH!WPr=MXL-%wzDwSy1yJ4l+&) zNCcZkc(&{M;lOjacoE@wgclGlqp!RG>Q(WhHb53&bxcV58krPDeaga_PP9SPQn-9* z3Dj#Kjt0<)7Ghk&gA4^sOE|Z2jI57>KvCr>Mb$3#H0U*|0rhmMGnE=l1?q)qq)R=6 znn1m%7tS{scRQTpqp&$218AQ~*X>K$dM=a5&ZXuOnQU%0v6ZgZ60^1WT)Lji*>%&- z-Aeo4AD81nanXnkoI(F92rCHF2PROcN^P@ap&lG(1x;9c{A+FZ!G* zfO-SO!H|w~Mtb5i#n6ZX92z0K0QvbWoBx@?-G~-f-TkHrYMah1*D5tyYs`bv&ura9pSZmUI;iO)+dvl2yN?ubB(_W5Um( z-%H}5IU4qFhg;+sevWQh_#8Oqyz^LI z$Ba(igz-lp-Hb*s8EErlr`F;OXxHxHV}4PeebTThPWYQbv(A!-;(|4#R>4$^PlFWV#==Jse1aTtV+n1@%T7SY9y{3JKfbc@@>(&!UM-dLOSf-Y-H<2Kz#mX-)Kn`&3J_ U6l>6m8lgTSJpo+?Q3vIJ0EAL|%>V!Z delta 1179 zcmZ8fOH5Ni6z#O{-`DoFrM3tKiXv7N1Vj-LEa0aJDJX~u3Bk_{Nn@ch4~@~dFyO+K zF?YjGUAiQ$-I%y^;m&knbm!7U*6O{}`U!33-1}zc{?3_q-QPN+)kwq%;7@!1wtT>S z9&Ke)J3aj@!&^l=&kAov<88csOIz&V9dm(v?#&jP48lvthF#Z#FCh&UVolS+5(d{t z1HwdWptaGOjc==AZL|&#Z88z&j?j5n=$PH*ku7bLZL&p|N3lW-7PS`pHLL03{)W5( zpS5{d*B`JhSWhP5m!4rc$QawK2X>5{lfl-VfWxdH>>4j%+sq!82XU{Iw`B^g+((cm z&h|@%of%U-(yYCXRUZA=y-_WB*7^xlxZGBmkxYl%L-_W{uYyi zjW8R2SWi=@DPV-)48bVDS&DBNMwj+{7vWF5$}o%>vhdX|vQFr6Iz75s?=whVABCsK z(4EBPTL@0T!qP62W2!ic!7jkZP?DrLC@hY23Y+V~D?dO169~01s=DUixV2HPu1T7y=E$;td$}U+iHaPDSCOS5B@<~=*NVuOiWiN% zJ_^!qR;v|B6xFm!+224iup9k6Zd`-eXpWs%wVy>t9N9`ymk?k*o`gHD9#(@<9@$G_ zsTp)varp)U4T0>=s00(SVqC?K)9rHzrMv~#-IS+t{8Otg)GXb?)es6_CZ(EHsk-A! z$r7DYlS<-RsJylT*PRr2F)e$a;_3~!6m=QDYr21{Dg}!4Aud}L5qsY!-pVt1RY|v& zNU%rnIFU9NaJ!Vx!HdMjVKpc{T)Ce>5&Mr*dRvlItoQZ*%WX@h2mZe4T701}U7T8; zzfvg873YgHr8!BwNi{PyTUToV8BL$9S);2}ezzjV7@qh?5ZnNb<+4mL5%MBlG7(Og Vp5+-{)QdWy1k-+^uY1Nm{WtDv<&gjY diff --git a/fraud-alert-service/src/models.py b/fraud-alert-service/src/models.py index c13daff3..49fcdf15 100644 --- a/fraud-alert-service/src/models.py +++ b/fraud-alert-service/src/models.py @@ -20,7 +20,20 @@ class MerchantCategory(str, Enum): class TransactionCreate(BaseModel): - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict( + extra="forbid", + json_schema_extra={ + "example": { + "amount": 149.99, + "merchant_name": "Best Buy", + "merchant_category": "electronics", + "location": "Charlotte, NC", + "timestamp": "2024-05-01T14:30:00Z", + "card_id": "4111111111111234", + "account_id": "ACC0000000001", + } + }, + ) amount: float merchant_name: str @@ -71,7 +84,15 @@ class StatusHistoryEntry(BaseModel): class AlertCreate(BaseModel): - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict( + extra="forbid", + json_schema_extra={ + "example": { + "transaction_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "risk_score": 0.85, + } + }, + ) transaction_id: UUID risk_score: float @@ -117,6 +138,13 @@ class StatusUpdateRequest(BaseModel): changed_by: str +class SummaryResponse(BaseModel): + total_alerts: int + by_status: dict[str, int] + by_risk_level: dict[str, int] + avg_resolution_time_seconds: Optional[float] + + class AlertListResponse(BaseModel): alerts: list[AlertResponse] total: int diff --git a/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc b/fraud-alert-service/src/routes/__pycache__/alerts.cpython-310.pyc index 31df117b6961d67f5db5e836cee82d19336f48a5..83c38def6a1b415793d1257e1098e0972ce4a638 100644 GIT binary patch delta 2971 zcmb7GU2Ggz6~6cG?C$LU`u`_(vT+F$hu zX4Z+_U2TdTf`qIpnx{Ura$b{FSlko|ICLtE#i0&vieDVZFQ(*l_(dSsr+ScoLKcH{rvv_%{)69N)$(#i&ISS}Bwu93Iqxt0J3QlE z77z2e#b%>n!0HXs94e?%I}u7;^mRicxPW zIFI9E0xl6rH|h%9vMJ#PeR~R_lrt^UHH~_?Yl^Q`Y|DakfxLQv_L)`<=1G2!k;F|` zKpCzo8+8q}zT{m>U7L6V8AT2kf|nncs%kXrE(ANE<>1Wr&r`n;{N?R>c9>s8sw&U0KDRy?Y|o z_X)}bquyuvsmmo2)xf^*l#-xU6nc2cG+~Iy5hRy{`v4Y5exF-Yb@Vwp6@BW|yw`eG zJ2pzK=_j2qEh{+NxzaeIZCu31ncxh|Ly8UNvP)P_Z6+hZj0ZC=wYI3ScElCy?7cf} zt)*>6Wb`TD(w?IHo7daXmXNXMSzEMND=On7EXuIpI!KDJdDh}c)Vstk&Fu=}VzE}t zjjk|gxUsD`N+w3wQ{Ildx=hLxnv`h8*8@W&TNiD;rK4nWD}|EjKr#``beRce$yQ`5 z-Ac9+!>qHFd5WqyTOPlKN0Ns3dW17eKgFB}_mNKbZG2i0ck8C>N4n#7s}WbE22i?4qB)r+qe9M^E0&TIip=k0PG)3H8V2+LQN4R^N7e|82G zF5kFzr4Xjz=e$GOb}N{w>_B53WH^%u?~?m6ZH~!-{vW|ibU7x5Xz`mErDYMExFS$_ z6?a2947~(z5$r36#C)fwEcIXs7JiFthXN%YPO!DZ2RrncFnpDi9YrW=e%iHNEWmI_ ze&Vgwa_A60gTq~)lnxmywK6Ejt~corFS}+#l{=2YBT+h7^MR>ZQoM6^K#it>`8y|u zuc5)NuT$NFqa;uc_?ar$4b!o!HZ%+uR6q+2>#pEOFhSrhDeOTgreO_V9e9T*s|cn1 zKI0sqa|SGeAVhScl(MdZhH2rBmF2>!AMb{mEW*95C|Qc&PWQVywa25sL_H3j`dM)` z%Qd`p(aUw-k9S(=Jj(^omk)3P}0<>I0E)7Za;dxa24r#to!SVs+Osgv(KYdmeZ z?ft&z{mxaS!)pkC%e_J;UBR;5Le*|s623*OZxT>fgF6V=QAX(%-Q)IK#H2q#{e5)DBG{mh0xJTKUhz)%Tb&()ikTpq zVF!(nC>n3Q>hj9nvC=?XNOdw=9 zj#;zH28MXmaXhTbbt(fZmo(@X69vqvY6TU&y*9p#3`^DdrAb1D(1Ad5nKaQQ*zK?Yj z^vi>SnrSXc`WFeyT5#{l@P+)3NjJY-EQXd`_x?D%*?|G1mf{Xn=BOVJlKvRjO)QzP z$I}m!uPxb*3vb{MenfDMfckKd(eMaIessye|1Ee-R9b3JqV0xCpKsW*Syu~m96Q$; j9uzv$v2@a+U!=u6Pv^WZMowpwJUubR(;2L;;bH!7)xV*^ delta 1677 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 MUDtf*(S4qO0f%CCw*UYD delta 1134 zcmZ{jUr1AN6vubAp>sFy)VsOOoX%;vYqKGt5+M~8l|@lWJ|x6+Yfjy!zfIFM|3#uk zigKVRe5kjA2x>$WM1&MQMh`u#dXNZ;UKTwEopWc35B)BDxWDu7cYf!7@6)c;3v#@; z*sP;<{`t#DTg7AfVF~7}#d;ZVS*pYiWwaz%G;T;6()tnIxG`-^Ok+@~m->>1yQZ}M z9*8*JR9YNC-8r3VJ|!GfDr@c$p+GVeyckGgr?twKH4hI4*A3zjMW}^B#y7Q1_@S&> zHj%*uWiSe95IyKGaAjJQTcEU)J+bK=0TIH0*OmEWO@ieNJB@@gSOgKlsq%07unuz$ zH_qA3!1qYlqOBcob<~n8$i)NObCqW5tcK&1; zWOl^yfVmQ-?QbEBla>1@-CNqrsrfUsCIrX0#2}efDL;wuo!h*A?H5C26`b2G#rQSB zNK%Fii(K!usvhXWr3wf3mMG}0Dlbh^$od@|+{1|0Lx0sOTta{Kt}Quyod?x6Na3q$ zJB)FpRa~Y5s(eZ)d=`HgaS03ETN7jMM(G;6QjGEcCw_GAlz1+J)uP?xU%xSTt!?vz zFK!f@z*J2uOyZLo&%r6Os`~IAZGGJ@hV%PlYbooaqdzwG`Ek+yO5RVTm7_h{Jl_kn zA*Rt#`x>T6KJBoAC3T138h%n;nW4JFP?RlP?*w1$IIcXQzfhFz$6Ra4Ff5Z*a<4^qn3$~U@EZLjZb@@znN~>H!Q^`W+lUk#f zw0m>Iq5>~k>{#V1khCUwj3S7BpN)zf@u@I}abJh?9hnm{fz|JT81&IY|IZ?R_RU0k zI4Q)4F!~u$Mw}rSEIl#87-NhxCJ0$$G$8u3(qKRYqM@XSq(VYd&pb!mU@R~+5m>Yz N?Mq`T6rydn`!4|E81?`F diff --git a/fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc index f3f311e0bef906874e149fb216fd73699c738be7..853d4fee7ee819c83fb409a1ad152566e5ee44c7 100644 GIT binary patch delta 3246 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 xAaDpc42%GyzKr-1f6Uf$h;9oDwRpkHx diff --git a/fraud-alert-service/tests/__pycache__/test_state_machine.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_state_machine.cpython-310-pytest-8.3.3.pyc index 59a3df36cb3ddfa8fceee0c9b7193eff92775420..af9e651720bfd8773769a3207272a3b28b2332c1 100644 GIT binary patch delta 4240 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 delta 4289 zcmcInT})iZ72a9)=k6aYEbCoh7j}UHmt`^L$2PICF?K>?Q*3HlCA7}wnz`VOm)-T; z3o&#lal=DuH%`esMa=_5QL24#qKc|ftM;L4t16Y+hpJMQTUlzQ_Q6v7koKW!AEKT! z*IjNfxK`Y<(l>L@Idgty&YUy+^U^1m0);@pYlFW({OHHoA8rKV$C{m1Q3rKW z7j;vKdfsx+xT%-=)@?HqeUb)f^*WjH(5GmS*22sSvpO1rnUB`fFi8370UCiZK%=w) z#%kI~n_#S=F&c+4sJdRP-K?X{^ETS@HrcY%1Wm5nULi9f+DhBjK%}0w)6c;ewup8J z(N5YWL=RXvvfy;nl;A`xoF2hR(_X=e(u1@Qv^LN~v>(PsIzR_uY@$Q-FpM#JgdSZV zu+7BjFnt0D&GZ;O4r2=)p(kKW(35l&#w0yO$6#!wr|B6O+xSg#nauFNl3&*OU_WJI zxMp)%HLsU#tcHJR-w0F#S+fJw5mw9Rt5&CQ=jAF^3j^~bh?P|gC0Q82uYgs*8$m1fR5xh%o zZ#c&Z3Gt6y>!gK0?;i1?LDr0rW~-LP^$+(1Fm2(1p;0kVX)J^y2IwLO;R)!XUy4 z1grgDcx95}AA7@O0xhnwvmhY{Cz)v#K8)%)C8w#GVgYwyNoD_YC(uZnu4;N_k-zPW z`-G>TM^Df1=aWa(W|bUd4*mbQ(`>qvrz*>E)gSVWpgY2+DPC@iZ*Tg0?Sqv)TJbS; zDXYGuvh?t9nXm+gRITZXzNlrcWHr6Omha(hVeQ;rb22F7v>V|NLYhz2G`H+~atV}i z^V>D8VM~r75mStPiGQ~?)ld$Y*?ba6hJz})!bXuh#y!F2{xeARj~16Ra|O1b=!g2) zlgNDvVI1LU1Z!qI7fkgdTf-}dnHeRUn3+T;>@tuH7nqn=b-0x0aP1_*1^$;vjQ=M1 z{t)iXy*CNa9-6OZ9}_VparF>J5-oeoNM@0Q;h1?o%3laYKXrFvb#m=A@HH2JWDk)ghf&n=P%oGYB4uf32lq4iYG|bP%N3JwS;1shnEYGlS5H?=8t$T+ zr`i1cBiM!#vFlWE8+w9Q&}-{~oDQdn!mouxksPp~dcBAetq7LA{~&yKyyAy>GLI1d zRn#(vV72)B13l?15dV(#T(VB0U-DMF%E^`f?e z2$jMtThCt{eOO%U2TJv&#_rH9SgJ_?5}bo*6ij&aLE{r-t#a)Ds<<{h{5Y=d-7~C9?_)?L+v6EJ+9C(Y-jf5oqdguDYo0qPX->@NI^7u?P)Fr5Qz^|7!hv<9}RPKgmM;V&Vwt z@FKC~QQ6pO@5;mkGNeuG5WV2MJ!pRWk_f2w&uV z9nloVXTGsS_1)_}d(C<-+VWYr-PO26I68{6F@y}l3kYWr6oiWN`!wW2n_T@<5H#Bz zG~*?%BtzRjXuD{aL#RNsf+)_ufN%~0gSc0Pvz;Ku{(BH++|#)3D+remtULI6$070| z|4By!S;u?6{STP=cDy*po4Stny8p9Ewj1`|K7*)M9l$ia$8+#Hnk_68Pkh90be(XC zO>gHv@0uLic_fJ%j5)LO&XohZyQavRZ{Q2!ux#z*A~9aY#itNP5kx|WowvqRmRlpg z1@`@{z;;&;S@AQ%Zi-s=iF{|L#;*9a#iGiVvRZ+qkC;0yoEI6BKr3R$_s9@<;Oy%7 zyE7hFR}$X(F6p-9xk#jkJm98N?V-CEj@d8p0k=fhYvQ=RlNzw}3${nnuw6;AUoZ?q zHC!4rLUn=NLeIa6VA)s{=oAg3h|lE0{WmdxE*&O+;)QfL@j8gegw>e?dLy?R(&fc$ zPS55wc9g%99$3SQV7Q9QsEGyO!~9=!*H%7*j0uGE2vZ1`0Sw22!mb*=qQaB~RcF~1 zmCYj)A2=+BkVnAx1-pj85OjnkgbUXZu_Ut_2wz87N4Sab8Un^)iCpYK5NL*Ue4#)W bbLt7)Rc#WW)yD7ip7sPJm*kT?e)qotE5SAp diff --git a/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc index 4bb438aae8a2d584fb582b9168f32b902d381f66..44b45f032568981cbc1efc0ee89d7dd485c25adb 100644 GIT binary patch delta 1323 zcmb`FNoZ4H5XaxVye)ajYc{jANo(suW7XPp!KK!%DhTe@sx3(DOEisX!uJANdBOGM zcJRB3GvsAXa5twgxM%8OfAvIR~ap{+w8o(1-gPIIC0xQ@JTdgsNhy zre-U7z`a^gFsj;W4KBh3Fa>_Z=Q)Cf+_Qyf{kf3!Z-w|CUzlHwv~0NhAM$*`Jn#NF zPlA`D9+3IDXnQx!q~R)0_kA%kmX&3dg-bfi(*r4soC8duk*Nd?k<$2At(m`%MwW!g z*F#uCND)mepJIG{6qN%ey+%i9w5AmIe_p;1)<+9yVhi0`;n>zl`uGcrs>kiNAPWhMpp$qXh{+0UndF@y|5(tR;I62aUex45P zay#~3w0X+graRY8H_p~Mf7({#{B0;4+KDcUhUJ$HxD1zo<*;<3-VvUG0Hv_#R)P?w zTPZl94-2k9$8q#Bx;5q}Y#?kTY$j|$@W;^v0UGI1hn{`LacFdf*6 zO~v_eZ1I9)XgX61LTqmQQ}t>r0+{TcuZKU0_cl>NOG0?4tx{cWJ}ts=elC&p|K16| zpBU&miPDstEyab=Sym+c`V{9HF_Oda5{4}U5fo!EiET8CQ-JS~@j`NJWQY!LBkUuL z5Jm}y2!{z{h=x?mutGyGXIQ3~t+4!=EXz@_K$s#>9mWXDovEyB?TWfJRa1U^e#Ikc JI_@>E{2gLCB)I?p delta 1212 zcmZvbO>7fa5XX1c`{iAG?R9LE7)$~RX%<95&Ig6?VU!>chydY3K!u9swHpIToY@s> zB1Z_+6KX5LyH*teA;bYB?6jxiaZ{qJ^89J-H|LT4rwaFPYV%q@<#0gU)9O$V?fRXrJ(MT|OFvyY{TlQP z)8jvs#A79QWkfo+e=gF9G~~9XW+q`KaS z%mZ$r$lz!aJk8UW6homxCPgt+P(%vdGk%t{(wyo=J+qW-;R$UFUIB}YF9VY&%4u&nKT$ZMY8K1D2ak z<_S1wxxm|zf;4Z(IQhso|IJ0Lf z)n-&h&g}m3TMAoxwGx!aD;|dCT&{)=`8!(P3)@7elNH?^-I?=L36D1g?bQ5}fPnSp zxsEJARlJM!h_|*S?&kUu<1ADNCcndU^L_3+89Z@5pHJ2(ix(Id8JEP5`N2&S6uQxc zHf3)($ymV`PKn?1BOR>w`F&DyJ6s?mIXN^Fh(l|?J{q3l2(NI2F~&H-ILSE0I8A6N v6D7RZvg#!)O;iG`%~r6?MRt#M#xx@kf3F>0r5n1TE!;`XR1Aw^Bc^@?<~Ibm diff --git a/fraud-alert-service/tests/test_alerts.py b/fraud-alert-service/tests/test_alerts.py index 2e4a0e24..af7112f5 100644 --- a/fraud-alert-service/tests/test_alerts.py +++ b/fraud-alert-service/tests/test_alerts.py @@ -65,10 +65,14 @@ def test_create_alert_initial_status_history(client): def test_create_alert_has_created_at_and_updated_at(client): + from datetime import datetime tx = create_transaction(client) data = create_alert(client, tx["id"]).json() - assert "created_at" in data - assert "updated_at" in data + # Both timestamps must be valid ISO datetimes + created = datetime.fromisoformat(data["created_at"].replace("Z", "+00:00")) + updated = datetime.fromisoformat(data["updated_at"].replace("Z", "+00:00")) + # On creation they should be equal + assert created == updated def test_create_alert_embeds_transaction(client): diff --git a/fraud-alert-service/tests/test_filtering.py b/fraud-alert-service/tests/test_filtering.py index 0ca24d36..4e86eaee 100644 --- a/fraud-alert-service/tests/test_filtering.py +++ b/fraud-alert-service/tests/test_filtering.py @@ -29,7 +29,7 @@ def assign(client, alert_id, analyst_id="analyst-1"): return client.patch(f"/alerts/{alert_id}/assign", json={"analyst_id": analyst_id}).json() -def transition(client, alert_id, status, changed_by="system"): +def transition(client, alert_id, status, changed_by="analyst-1"): return client.patch(f"/alerts/{alert_id}/status", json={"status": status, "changed_by": changed_by}).json() diff --git a/fraud-alert-service/tests/test_state_machine.py b/fraud-alert-service/tests/test_state_machine.py index 98681329..4d752dcd 100644 --- a/fraud-alert-service/tests/test_state_machine.py +++ b/fraud-alert-service/tests/test_state_machine.py @@ -1,3 +1,5 @@ +import time + import pytest VALID_TRANSACTION = { @@ -180,7 +182,6 @@ def test_assign_to_escalated_returns_409(client): def test_assign_updates_updated_at(client): - import time alert = create_alert(client) original_updated_at = alert["updated_at"] time.sleep(0.01) diff --git a/fraud-alert-service/tests/test_summary_stats.py b/fraud-alert-service/tests/test_summary_stats.py new file mode 100644 index 00000000..a603a3b8 --- /dev/null +++ b/fraud-alert-service/tests/test_summary_stats.py @@ -0,0 +1,204 @@ +import time + +VALID_TX = { + "amount": 75.00, + "merchant_name": "Amazon", + "merchant_category": "other", + "location": "Seattle, WA", + "timestamp": "2024-05-01T09:00:00Z", + "card_id": "4111111111119999", + "account_id": "ACC5555555555", +} + + +def make_transaction(client): + return client.post("/transactions", json=VALID_TX).json() + + +def make_alert(client, risk_score=0.5): + tx = make_transaction(client) + return client.post("/alerts", json={"transaction_id": tx["id"], "risk_score": risk_score}).json() + + +def resolve_alert(client, alert_id, terminal_status="confirmed_fraud"): + client.patch(f"/alerts/{alert_id}/assign", json={"analyst_id": "analyst-1"}) + client.patch(f"/alerts/{alert_id}/status", json={"status": "under_review", "changed_by": "analyst-1"}) + client.patch(f"/alerts/{alert_id}/status", json={"status": terminal_status, "changed_by": "analyst-1"}) + + +def get_summary(client): + response = client.get("/alerts/summary") + assert response.status_code == 200 + return response.json() + + +# --------------------------------------------------------------------------- +# Basic structure +# --------------------------------------------------------------------------- + +def test_summary_returns_200(client): + assert client.get("/alerts/summary").status_code == 200 + + +def test_summary_has_required_keys(client): + data = get_summary(client) + assert "total_alerts" in data + assert "by_status" in data + assert "by_risk_level" in data + assert "avg_resolution_time_seconds" in data + + +def test_summary_all_status_keys_present(client): + data = get_summary(client) + for key in ("pending", "under_review", "confirmed_fraud", "false_positive", "escalated"): + assert key in data["by_status"], f"Missing status key: {key}" + + +def test_summary_all_risk_level_keys_present(client): + data = get_summary(client) + for key in ("low", "medium", "high", "critical"): + assert key in data["by_risk_level"], f"Missing risk_level key: {key}" + + +# --------------------------------------------------------------------------- +# Zero-alert state +# --------------------------------------------------------------------------- + +def test_summary_zero_alerts(client): + data = get_summary(client) + assert data["total_alerts"] == 0 + assert all(v == 0 for v in data["by_status"].values()) + assert all(v == 0 for v in data["by_risk_level"].values()) + assert data["avg_resolution_time_seconds"] is None + + +# --------------------------------------------------------------------------- +# total_alerts +# --------------------------------------------------------------------------- + +def test_total_alerts_count(client): + for _ in range(4): + make_alert(client) + data = get_summary(client) + assert data["total_alerts"] == 4 + + +# --------------------------------------------------------------------------- +# by_status counts +# --------------------------------------------------------------------------- + +def test_by_status_counts(client): + a1 = make_alert(client) + a2 = make_alert(client) + a3 = make_alert(client) + + # Move a2 to under_review + client.patch(f"/alerts/{a2['id']}/assign", json={"analyst_id": "analyst-1"}) + client.patch(f"/alerts/{a2['id']}/status", json={"status": "under_review", "changed_by": "analyst-1"}) + + # Resolve a3 + resolve_alert(client, a3["id"]) + + data = get_summary(client) + assert data["by_status"]["pending"] == 1 + assert data["by_status"]["under_review"] == 1 + assert data["by_status"]["confirmed_fraud"] == 1 + assert data["by_status"]["false_positive"] == 0 + assert data["by_status"]["escalated"] == 0 + + +# --------------------------------------------------------------------------- +# by_risk_level counts +# --------------------------------------------------------------------------- + +def test_by_risk_level_counts(client): + tx1 = make_transaction(client) + tx2 = make_transaction(client) + tx3 = make_transaction(client) + client.post("/alerts", json={"transaction_id": tx1["id"], "risk_score": 0.1}) # low + client.post("/alerts", json={"transaction_id": tx2["id"], "risk_score": 0.7}) # high + client.post("/alerts", json={"transaction_id": tx3["id"], "risk_score": 0.9}) # critical + + data = get_summary(client) + assert data["by_risk_level"]["low"] == 1 + assert data["by_risk_level"]["medium"] == 0 + assert data["by_risk_level"]["high"] == 1 + assert data["by_risk_level"]["critical"] == 1 + + +# --------------------------------------------------------------------------- +# avg_resolution_time_seconds +# --------------------------------------------------------------------------- + +def test_avg_resolution_time_null_when_no_resolved(client): + make_alert(client) + data = get_summary(client) + assert data["avg_resolution_time_seconds"] is None + + +def test_avg_resolution_time_single_alert(client): + a = make_alert(client) + time.sleep(0.1) + resolve_alert(client, a["id"]) + data = get_summary(client) + assert data["avg_resolution_time_seconds"] is not None + assert data["avg_resolution_time_seconds"] > 0 + + +def test_avg_resolution_time_multiple_alerts(client): + from datetime import datetime + + def resolution_seconds(alert_id): + alert = client.get(f"/alerts/{alert_id}?show_pii=true").json() + created = datetime.fromisoformat(alert["created_at"].replace("Z", "+00:00")) + terminal = next( + e for e in reversed(alert["status_history"]) + if e["status"] in ("confirmed_fraud", "false_positive", "escalated") + ) + resolved = datetime.fromisoformat(terminal["timestamp"].replace("Z", "+00:00")) + return (resolved - created).total_seconds() + + a1 = make_alert(client) + time.sleep(0.05) + resolve_alert(client, a1["id"]) + + a2 = make_alert(client) + time.sleep(0.1) + resolve_alert(client, a2["id"], terminal_status="false_positive") + + a3 = make_alert(client) + time.sleep(0.15) + resolve_alert(client, a3["id"], terminal_status="escalated") + + expected_avg = ( + resolution_seconds(a1["id"]) + + resolution_seconds(a2["id"]) + + resolution_seconds(a3["id"]) + ) / 3 + + data = get_summary(client) + assert data["avg_resolution_time_seconds"] is not None + assert abs(data["avg_resolution_time_seconds"] - expected_avg) < 0.001 + + +def test_non_terminal_alerts_excluded_from_avg(client): + # One resolved, one still pending + a1 = make_alert(client) + time.sleep(0.05) + resolve_alert(client, a1["id"]) + + make_alert(client) # stays pending + + data = get_summary(client) + # Should still compute avg from only the resolved one + assert data["avg_resolution_time_seconds"] is not None + assert data["total_alerts"] == 2 + + +def test_avg_uses_terminal_history_timestamp(client): + """Resolution time should be > 0, confirming it uses transition timestamp not updated_at.""" + a = make_alert(client) + time.sleep(0.05) + resolve_alert(client, a["id"]) + data = get_summary(client) + assert data["avg_resolution_time_seconds"] > 0 diff --git a/fraud-alert-service/tests/test_transactions.py b/fraud-alert-service/tests/test_transactions.py index c5aa9957..706e0a5a 100644 --- a/fraud-alert-service/tests/test_transactions.py +++ b/fraud-alert-service/tests/test_transactions.py @@ -31,8 +31,10 @@ def test_create_transaction_returns_generated_id(client): def test_create_transaction_returns_all_fields(client): response = client.post("/transactions", json=VALID_PAYLOAD) data = response.json() - for field in ("amount", "merchant_name", "merchant_category", "location", "timestamp"): - assert field in data + assert data["amount"] == VALID_PAYLOAD["amount"] + assert data["merchant_name"] == VALID_PAYLOAD["merchant_name"] + assert data["merchant_category"] == VALID_PAYLOAD["merchant_category"] + assert data["location"] == VALID_PAYLOAD["location"] def test_create_transaction_missing_required_field(client): From 971af8b36932dbb6972c8b1d0d71537399179c82 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 19:29:05 -0400 Subject: [PATCH 14/17] Updated the README.md to include more details that may be helpful. --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 75a0f838..95e41cbc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ I put everything in fraud-alert-service to keep the project separated from given To test this, just follow the steps below and test out the interactive docs at `http://localhost:8000/docs`. +## Tech Stack + +- **FastAPI** — REST framework with automatic OpenAPI/Swagger docs +- **Pydantic v2** — request validation and response serialization +- **SQLite** — lightweight persistent storage via Python's built-in `sqlite3` +- **pytest** + **httpx** — integration tests using `TestClient` with per-test isolated databases + ## Setup ```bash @@ -41,3 +48,26 @@ python -m pytest tests/ -v ## PII Masking `card_id` and `account_id` are masked by default in all responses (e.g. `****1234`). Append `?show_pii=true` to any endpoint to reveal full values. + +## Test Coverage + +142 tests across 6 test files, each corresponding to a spec: + +- `test_transactions.py` | 23 | Transaction creation, validation, field storage +- `test_alerts.py` | 30 | Alert creation, risk level derivation, boundary values +- `test_state_machine.py` | 33 | Status transitions, analyst assignment, audit trail +- `test_pii_masking.py` | 22 | Masking behavior, `show_pii` parameter, edge cases +- `test_filtering.py` | 21 | Filter parameters, combined filters, sort order +- `test_summary_stats.py` | 13 | Counts by status/risk level, resolution time calculation + +Each test uses an isolated SQLite database via `tmp_path`, so tests are fully independent and leave no state behind. + +## Spec Driven Dev + +I have experience with spec driven development, so I essentially just used my standard workflow where I broke down my project into specs and wrote these out myself, used Claude to review these and enhance them if necessary (it's good at finding edge cases and such that I may have missed), then I added actionable TODO tasks before implementation. + +## Code Generation Tools Used + +I used Claude as my primary code generation tool. + + From 67c9a966d80ffa3c1d8bc0aa9f075aefbcc9c343 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 19:41:59 -0400 Subject: [PATCH 15/17] Remove cached files and claude settings from tracking --- .claude/settings.json | 20 ------------------ .../src/__pycache__/__init__.cpython-310.pyc | Bin 201 -> 0 bytes .../src/__pycache__/database.cpython-310.pyc | Bin 2140 -> 0 bytes .../src/__pycache__/main.cpython-310.pyc | Bin 702 -> 0 bytes .../src/__pycache__/models.cpython-310.pyc | Bin 5058 -> 0 bytes .../src/__pycache__/pii.cpython-310.pyc | Bin 655 -> 0 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 203 -> 0 bytes .../conftest.cpython-310-pytest-8.3.3.pyc | Bin 840 -> 0 bytes .../test_alerts.cpython-310-pytest-8.3.3.pyc | Bin 11829 -> 0 bytes ...est_filtering.cpython-310-pytest-8.3.3.pyc | Bin 14198 -> 0 bytes ...t_pii_masking.cpython-310-pytest-8.3.3.pyc | Bin 9582 -> 0 bytes ...state_machine.cpython-310-pytest-8.3.3.pyc | Bin 12025 -> 0 bytes ..._transactions.cpython-310-pytest-8.3.3.pyc | Bin 8016 -> 0 bytes 13 files changed, 20 deletions(-) delete mode 100644 .claude/settings.json delete mode 100644 fraud-alert-service/src/__pycache__/__init__.cpython-310.pyc delete mode 100644 fraud-alert-service/src/__pycache__/database.cpython-310.pyc delete mode 100644 fraud-alert-service/src/__pycache__/main.cpython-310.pyc delete mode 100644 fraud-alert-service/src/__pycache__/models.cpython-310.pyc delete mode 100644 fraud-alert-service/src/__pycache__/pii.cpython-310.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/__init__.cpython-310.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/conftest.cpython-310-pytest-8.3.3.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/test_alerts.cpython-310-pytest-8.3.3.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/test_pii_masking.cpython-310-pytest-8.3.3.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/test_state_machine.cpython-310-pytest-8.3.3.pyc delete mode 100644 fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index e8cc4f0b..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(*)", - "Edit(*)", - "Write(*)", - "Read(*)", - "Glob(*)", - "Grep(*)", - "Task(*)", - "WebFetch(*)", - "WebSearch(*)", - "TodoWrite(*)", - "NotebookEdit(*)" - ], - "deny": [ - "Bash(git *)" - ] - } -} diff --git a/fraud-alert-service/src/__pycache__/__init__.cpython-310.pyc b/fraud-alert-service/src/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a0bcca3ff98b80e4097f55d8fd3fc5cc3c98517c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201 zcmd1j<>g`kg83WvW`gL)AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yY#5-bb= DR(>|r diff --git a/fraud-alert-service/src/__pycache__/database.cpython-310.pyc b/fraud-alert-service/src/__pycache__/database.cpython-310.pyc deleted file mode 100644 index 3939d938fa6781fb6c45767f807528d0a002d9a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2140 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 H&z1fQu8uT1 diff --git a/fraud-alert-service/src/__pycache__/pii.cpython-310.pyc b/fraud-alert-service/src/__pycache__/pii.cpython-310.pyc deleted file mode 100644 index 6f6618292d0f831b9739a6fe9f2183567ce23847..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 655 zcmZ`$y^ho{5cW9UTrS*7r$a)bp`Z&=E)69@NKkLVL37Q@c;+CxStqic6^FJvD0u?l z0eA?+L(rwA+E+k`Wt^4Zgv3PtJTsY}Z^rF(nhTb1U%%7~5aN3@u8U&h5g&h_0R=)M zR&Wj=P?E%igwT2I4d zt(%^P)KTc2HB8};L_M~D%*Wql2x23y0AUF(LwUv&HUbkA@8oUqL40CQp=R5d@$PZdn|QBE|gqyT^p*rGgVjDr`_d{ue4vz#~~vzpE4YQf)j9Y@q-Mn z|3CB{j@XEW2*4<>V7!yKf{zmAd*FeSSs{l5qaDs`9CB?;+uOi8&z<%i>Oh5J7A;aU zH*I%0wsl8b*-aUEFE^qdx3Y&r)`sJecKG{Xk*tqF3NrrkU+$QVSuq)Mp7wOC@12af gi4ShyC5Bw9JoXsNXAd}`Z|Grsg`kf~6bwW`gL)AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yY=N{m|mnqGJ8L z#FC7}ysX6J{G?)im(=3ylKcYw;)2v<-ISutveZ1?l+-falGNf7bive|{DR!nyb}Gi zqQue^-Nc;Kq7q%8k!6|5srn!d#rpB_nR%Hd@$q^EmA5!-a`RJ4b5iX(70aq2-Rb;DWz~p|I=`pxRJ(K+ zBhg=sdCZfqXoKlpj$fla@*BFN!#(VxBW6EusMRD}}_?7naXiux|f!d24 zu~l2N0E;GkwslCO<$GB(<#72O={_X>sq;5RXp`5F$?63wo0-WEg!yQjHaD#(CtS-@ zQBSxyo%lii4+_<^74VhMHCyrtQ$pJbJn~eQBKHL*FPnPaVQ4R0Tz2ec(A$hb1)oU} z2Qel%4G4aCnTC4c0o-$Bwv@`s+PG}Ly(n%@_lx$Tdm1y)#>%FiYH_Be6|R31a>;cp zg<_vxFb$lDs+~(ET;iEga;5`l=HhJ%9nYDutd&z=upPAHlx9Yk(`)W2x%PAn)gASe w_>79ly5rv^b9ru;T6ADKrf(_#Z#eRF`wE}=ud1WsG0HFrGMstAQMiNu0j^uxzW@LL diff --git a/fraud-alert-service/tests/__pycache__/test_alerts.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_alerts.cpython-310-pytest-8.3.3.pyc deleted file mode 100644 index aa7d6dada352fa46e13f8c5968a0e310714bd7cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11829 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 bO5>nE+sdMzLM;_(vv&{Lwv|JRolE^M?2?A< diff --git a/fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_filtering.cpython-310-pytest-8.3.3.pyc deleted file mode 100644 index 853d4fee7ee819c83fb409a1ad152566e5ee44c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14198 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 diff --git a/fraud-alert-service/tests/__pycache__/test_pii_masking.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_pii_masking.cpython-310-pytest-8.3.3.pyc deleted file mode 100644 index 1cbc14f2d553c53237701ac569c8afb9e7d08217..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9582 zcmdT~-EZ606(=c*q$Jx(+%(^9oHS{zB>s>sJ4x5Hag!De+PPhmcAeIZqjhOVi6k1A zv{QRB_R&19+uL520?vRA@Z0`@0mJ@^f=>nZ)V&r1Ha~3pokL0@6*+ZRcOOc4?!ElF zm-n3WJ74w&2lEPk#XtU`w*QEt{Fw&HpM}9y9KNP1ico}FS1KyES|g(=s?yLaSv9(1 zR193v>Sn{LSU8svy2uJcn8Fe{k$+%Ta^j*Wh=KcRB`*fW(0!#+5c|Y_v;*RR7)CoN zo)8Do4v8nlA+-C%VQ~cQesNSBLwi6R7bnmTi>JiXXrB-##R%Gi;*>az_DS)KID__( zI4hn-dssXtM$sM-MR5-8QE^^eKzmG#iE*^Y#e|qddqR}N6xyd;vXdjuduHgn6f0r5B-J&Cu7xKOHR{*Kqh}Kx#^$e4@70mL}8(+9&Eprls6b zh1SZ5%tzX$_E5ow=<{2eEZ}ZA0J1(f5lE-$J5`$A-%>;Uj_);#nef27b8pYgLE6nPN$Y_Cqdl3{^$!!Fjf%*V=!FCE(s>3tPCZ=uhv z8n@a*RTvNSL&}CuIMLE;D*D;L#9dY^`%wK*{WP!j z$P|$>k&D|yjt@x*_yw1XKApV0mE$ME{MF?(5+B=<;Q_nq)a$l%mu1y!EIZN-2XNV5 zSgF;6TGI~=(#^Ub97GQ*o&TQ zIDzecuv~Yb2)MFkgaw)*T3{(Tm`)C6lY^OP5WVeEw8Uson(PZ~ymw4;UJ^XP2p!6V1Ti|0zCVf%nHVL2 z1AN^8T38PZAOw39EsN127o$b44_au17&vqsSsdmDd=#KZVLnvnKA*xT%F7_@3?C$! zECWS#{dIzf60jnI59%fGW3-JRBj*?r05ipr^V|iH6ce~onvQ{`Xh@dJ(=_KZM99Wg zbee_f^!mO8$dqP^KA)apJt*d7iJsvVm#H^RPbW10QgU#)!l)Bt!c=Nhibk=zms6wJ zUR68ZE82ChxfBc17*@arRd~68a}uhLUHdqCq4ODa&xN zR~*J!9LR4=x07ws04nm3lCPh4|!JSn3`40P;T>Z~W3G>dH zk9NJoJNYseBMArPD@0x;@*0sFM0yNT>2cfAb+;2cLxzbxNQ?I6EsRq8LSa`&d;W$& zXJ24wBTw?E@*Cx2=mz=WaL=GSkadJyka1`cv?|-m`Y#de0*tc^#-v@ORT=0DK`XU7 zw8~_yGAK-I8LU-iVBjj?JfT$tqglLN6Zt+f1$#eJ4Ak30shDCS)M}zf)B19Y;*l+j zwIs|exlyFCv%Xl7zG%S*p}FV=)!VL+uj3UF9=%zw<$M=QG8a2-$j z#92!{66wQx80^gVf8Z+dU2xp+QHIPgxDS_GWFO(a!8|cj+<%#Q0yi4m&yiI0<$gZq zJ_7Syaet6!8cNM{Z|`Sf?(dtI$FU2Q{N+m*XJ^XOQ*ciQaYBBF$lF9X_I$kFa%9RD zo?md29(#vK4;xQChK-TCO9Jr)v2du(!tY^_Vj=lY9n~FzNo=cbz+My?zYi&%Qef^# zZ844&IRiz8nIlCOIyM=K3nkWDStzs7GW^#9eG{gyWq_R~oo9CH@SC8-xCU>F{Ub7w zCS_b&CM&ZA|42tPWhIoE{2>Exk7g_Mne8WgKU>>O65qD1IPRSY3yElVx9mjhMPWAw zvho)7D37qScIToX`CZ%;W;q;|-=zn3tKsF2Xz;_a#d8}At`M$BseM=BX*QpHYD*<~ z0nn@RM;NB$B?a}{xZ0K%R(4Jrkc5cL$AIh(*pqx@ARkGvzsgDoDPZ~7N!_hj-AO)n z1NI~v88MKWX=U$cV%Zp+?<)58&`T6(Qa0vFAXnT-GAQX1g=zf^G$}fJPI5;bk4$wh zRJUhBG#;$Trf)BBdm8E8!uQZqI7ex_KT*N>+RbPwAo zU-gxESG1z!7A4spuho5hv8n#P*yA`{!%Xa^4oCbI7^nD~!u0*S#PnEW+o%qWC9@4w zC*19#`VGcelTjV{uN_c5$69NOyeRAn)oHc?o^3ES+fVm?HkN{+`N$f7pP1$ld7lUy z=iTb;59o;>66vwa=X=2SalG{Kd=msr7wbbwUx}DfF`LMD3^`n~IBF}T%c^q?A78?=5SnCv4!XQcGE-Bd~wmO21qHPxFKxHR{R|Aj#c=Gc1Jy?!Eqvp z-dTn3<7(S&>T$E-WU~W9xgWShD~Mqc7e60pn~0U!Re}>7M@HHv?t$5*mf~U~c9p0w z(RP)xxN7u^i+P805e%a6;xz*Y`F?U=ZlPMJ)%;{0r{=fQhCMlkqM7h(M`@FbMAEsP zq@qciuJ6Q&ULHKnJV-9iUaUJycFl(iyW;)=?~3y^Lj;|5jCbDk|LOQN9SMI^@hQw6 zrH?B*RcZT1rro;2jt=XNzG_tG04BYxPNE$WQZ zA8f23jqM4yTNg$dCbXpMFXPV{++wC%!=Wtx%zCmeOeN$q*tudM!f;AUgoVUg6X}U_ zxF*jNxj>``-(yQlzHB5j0rg5C$-I3zk;#ve_p_2R?!BNNWhXnXRnu$Q3!YbZQJrju zyXC2=Pw-@f{n|10m#SRD7=LHL=AVlZr$Vb;#g?@E@TeWty~m?66Kc`p(!D2Zfg9#J zr$RG6#~&XW(IcVh%j!6UIV${esCS5#ev}9mlDTv%U!We9DY?MJ^#e|pOUj%|%Dr(y zMN&jA1rhS~**j#Hfh`!Lqhy898;-mi7EqUmXNhX)Iuwf@=ms3zqd%1htrr@eSgE@& yLVOheD#&SNK+`gs+WlwzJzuc0mSySjKg%+#oMl=CYXH>1m4cPWKXbn|r2Y##E|zfs diff --git a/fraud-alert-service/tests/__pycache__/test_state_machine.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_state_machine.cpython-310-pytest-8.3.3.pyc deleted file mode 100644 index af9e651720bfd8773769a3207272a3b28b2332c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12025 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 DR|#nm diff --git a/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc b/fraud-alert-service/tests/__pycache__/test_transactions.cpython-310-pytest-8.3.3.pyc deleted file mode 100644 index 44b45f032568981cbc1efc0ee89d7dd485c25adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8016 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 VmX)#6mZ|>6Ogm|1QMYob{{fzNZ@vHk From 56a56d371aa5b8da7ac9bc3d5f2562a1b898f45b Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 19:42:28 -0400 Subject: [PATCH 16/17] Remove committed database file --- fraud-alert-service/fraud_alerts.db | Bin 368640 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 fraud-alert-service/fraud_alerts.db diff --git a/fraud-alert-service/fraud_alerts.db b/fraud-alert-service/fraud_alerts.db deleted file mode 100644 index fa0c20b7b16a87846947f6cec90575ecf5278843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368640 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

    |X(A}IS-@@IJIERWyQXl5h&y#L@lE;t*4;cmw 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

    1eL73m|9&<7nJ5bNb#1_-eVdYo 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 From 0c781a0c2af67e0787d59519564a74f3dad9f9b5 Mon Sep 17 00:00:00 2001 From: Nathan Jacobs Date: Fri, 20 Mar 2026 19:42:59 -0400 Subject: [PATCH 17/17] Modified git ignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7882cba6..b0e8167f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ fraud-alert-service/fraud_alerts.db __pycache__/ *.pyc .pytest_cache/ +__pycache__/ +.claude/