| 1 | import json |
| 2 | |
| 1 | 3 | class RestAPI: |
| 2 | 4 | def __init__(self, database: dict | None = None): |
| 3 | | pass |
| 4 | | |
| 5 | # Initialize the database with provided data or an empty structure |
| 6 | if database is None: |
| 7 | self.database = {"users": []} |
| 8 | else: |
| 9 | self.database = database |
| 10 | |
| 5 | 11 | def get(self, url: str, payload: str | None = None) -> str: |
| 6 | | pass |
| 7 | | |
| 12 | # Edge Case: Handle invalid URL |
| 13 | if url != "/users": |
| 14 | return json.dumps({"error": "Invalid URL"}) |
| 15 | |
| 16 | # Parse payload if provided |
| 17 | if payload is not None: |
| 18 | try: |
| 19 | data = json.loads(payload) |
| 20 | # Edge Case: Handle missing 'users' key in payload |
| 21 | if "users" not in data: |
| 22 | return json.dumps({"error": "Invalid payload"}) |
| 23 | |
| 24 | # Filter users based on the requested list |
| 25 | requested_users = data["users"] |
| 26 | users = [user for user in self.database["users"] if user["name"] in requested_users] |
| 27 | # Sort users by name |
| 28 | users.sort(key=lambda x: x["name"]) |
| 29 | return json.dumps({"users": users}) |
| 30 | except json.JSONDecodeError: |
| 31 | # Edge Case: Handle invalid JSON in payload |
| 32 | return json.dumps({"error": "Invalid JSON"}) |
| 33 | else: |
| 34 | # Return all users sorted by name |
| 35 | users = sorted(self.database["users"], key=lambda x: x["name"]) |
| 36 | return json.dumps({"users": users}) |
| 37 | |
| 8 | 38 | def post(self, url: str, payload: str | None = None) -> str: |
| 9 | | pass |
| 39 | # Edge Case: Handle invalid URL |
| 40 | if url not in ["/add", "/iou"]: |
| 41 | return json.dumps({"error": "Invalid URL"}) |
| 42 | |
| 43 | # Edge Case: Handle missing payload |
| 44 | if payload is None: |
| 45 | return json.dumps({"error": "Payload required"}) |
| 46 | |
| 47 | try: |
| 48 | data = json.loads(payload) |
| 49 | except json.JSONDecodeError: |
| 50 | # Edge Case: Handle invalid JSON in payload |
| 51 | return json.dumps({"error": "Invalid JSON"}) |
| 52 | |
| 53 | if url == "/add": |
| 54 | # Edge Case: Handle missing 'user' key in payload |
| 55 | if "user" not in data: |
| 56 | return json.dumps({"error": "Missing user field"}) |
| 57 | |
| 58 | user_name = data["user"] |
| 59 | |
| 60 | # Edge Case: Handle duplicate user creation |
| 61 | for user in self.database["users"]: |
| 62 | if user["name"] == user_name: |
| 63 | return json.dumps({"error": "User already exists"}) |
| 64 | |
| 65 | # Create new user |
| 66 | new_user = { |
| 67 | "name": user_name, |
| 68 | "owes": {}, |
| 69 | "owed_by": {}, |
| 70 | "balance": 0.0 |
| 71 | } |
| 72 | self.database["users"].append(new_user) |
| 73 | return json.dumps(new_user) |
| 74 | |
| 75 | elif url == "/iou": |
| 76 | # Edge Case: Handle missing required fields in IOU payload |
| 77 | required_fields = ["lender", "borrower", "amount"] |
| 78 | for field in required_fields: |
| 79 | if field not in data: |
| 80 | return json.dumps({"error": f"Missing {field} field"}) |
| 81 | |
| 82 | lender_name = data["lender"] |
| 83 | borrower_name = data["borrower"] |
| 84 | amount = data["amount"] |
| 85 | |
| 86 | # Edge Case: Handle invalid amount type |
| 87 | if not isinstance(amount, (int, float)): |
| 88 | return json.dumps({"error": "Invalid amount"}) |
| 89 | |
| 90 | # Edge Case: Handle negative amount |
| 91 | if amount < 0: |
| 92 | return json.dumps({"error": "Amount must be positive"}) |
| 93 | |
| 94 | # Edge Case: Handle lender and borrower being the same person |
| 95 | if lender_name == borrower_name: |
| 96 | return json.dumps({"error": "Lender and borrower cannot be the same"}) |
| 97 | |
| 98 | # Find lender and borrower in database |
| 99 | lender = None |
| 100 | borrower = None |
| 101 | |
| 102 | for user in self.database["users"]: |
| 103 | if user["name"] == lender_name: |
| 104 | lender = user |
| 105 | if user["name"] == borrower_name: |
| 106 | borrower = user |
| 107 | |
| 108 | # Edge Case: Handle lender not found |
| 109 | if lender is None: |
| 110 | return json.dumps({"error": "Lender not found"}) |
| 111 | |
| 112 | # Edge Case: Handle borrower not found |
| 113 | if borrower is None: |
| 114 | return json.dumps({"error": "Borrower not found"}) |
| 115 | |
| 116 | # Update IOUs by properly netting amounts |
| 117 | # Get current amounts |
| 118 | lender_owes_borrower = lender["owes"].get(borrower_name, 0) |
| 119 | borrower_owes_lender = borrower["owes"].get(lender_name, 0) |
| 120 | |
| 121 | # When lender lends to borrower, borrower owes lender more |
| 122 | # So we add amount to what borrower owes lender |
| 123 | new_borrower_owes_lender = borrower_owes_lender + amount |
| 124 | |
| 125 | # Net out the amounts |
| 126 | if new_borrower_owes_lender > lender_owes_borrower: |
| 127 | # Borrower owes lender more than lender owes borrower |
| 128 | net_amount = new_borrower_owes_lender - lender_owes_borrower |
| 129 | # Clear all old entries for these two users |
| 130 | lender["owes"].pop(borrower_name, None) |
| 131 | lender["owed_by"].pop(borrower_name, None) |
| 132 | borrower["owes"].pop(lender_name, None) |
| 133 | borrower["owed_by"].pop(lender_name, None) |
| 134 | # Set new entries |
| 135 | lender["owed_by"][borrower_name] = net_amount |
| 136 | borrower["owes"][lender_name] = net_amount |
| 137 | elif new_borrower_owes_lender < lender_owes_borrower: |
| 138 | # Lender owes borrower more than borrower owes lender |
| 139 | net_amount = lender_owes_borrower - new_borrower_owes_lender |
| 140 | # Clear all old entries for these two users |
| 141 | lender["owes"].pop(borrower_name, None) |
| 142 | lender["owed_by"].pop(borrower_name, None) |
| 143 | borrower["owes"].pop(lender_name, None) |
| 144 | borrower["owed_by"].pop(lender_name, None) |
| 145 | # Set new entries |
| 146 | lender["owes"][borrower_name] = net_amount |
| 147 | borrower["owed_by"][lender_name] = net_amount |
| 148 | else: |
| 149 | # They cancel out completely |
| 150 | # Clear all old entries for these two users |
| 151 | lender["owes"].pop(borrower_name, None) |
| 152 | lender["owed_by"].pop(borrower_name, None) |
| 153 | borrower["owes"].pop(lender_name, None) |
| 154 | borrower["owed_by"].pop(lender_name, None) |
| 155 | |
| 156 | # Recalculate balances |
| 157 | lender_total_owed = sum(lender["owed_by"].values()) |
| 158 | lender_total_owes = sum(lender["owes"].values()) |
| 159 | lender["balance"] = lender_total_owed - lender_total_owes |
| 160 | |
| 161 | borrower_total_owed = sum(borrower["owed_by"].values()) |
| 162 | borrower_total_owes = sum(borrower["owes"].values()) |
| 163 | borrower["balance"] = borrower_total_owed - borrower_total_owes |
| 164 | |
| 165 | # Return updated users sorted by name |
| 166 | updated_users = sorted([lender, borrower], key=lambda x: x["name"]) |
| 167 | return json.dumps({"users": updated_users}) |
| 168 | # Handled Edge Cases: Invalid URL, missing payload, invalid JSON, missing 'users' key, missing 'user' key, duplicate user creation, missing required fields in IOU payload, invalid amount type, negative amount, lender and borrower being the same person, lender not found, borrower not found |