module transaction.api; import handy_http_primitives; import handy_http_data.json; import handy_http_handlers.path_handler; import slf4d; import std.typecons; import transaction.model; import transaction.data; import transaction.service; import transaction.dto; import profile.data; import profile.service; import profile.api : PROFILE_PATH; import account.api; import util.money; import util.pagination; import util.data; // Transactions API immutable DEFAULT_TRANSACTION_PAGE = PageRequest(1, 10, [Sort("txn.timestamp", SortDir.DESC)]); @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions") void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE); auto responsePage = getTransactions(ds, pr); writeJsonBody(response, responsePage); } @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/search") void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE); auto page = ds.getTransactionRepository().search(pr, request); writeJsonBody(response, page); } @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/:transactionId:ulong") void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); TransactionDetail txn = getTransaction(ds, getTransactionIdOrThrow(request)); import asdf : serializeToJson; string jsonStr = serializeToJson(txn); response.writeBodyString(jsonStr, "application/json"); } @PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/transactions") void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) { import asdf : serializeToJson; auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request); ProfileDataSource ds = getProfileDataSource(request); TransactionDetail txn = addTransaction(ds, fullPayload.payload, fullPayload.files); string jsonStr = serializeToJson(txn); response.writeBodyString(jsonStr, "application/json"); } @PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/transactions/:transactionId:ulong") void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) { import asdf : serializeToJson; ProfileDataSource ds = getProfileDataSource(request); ulong txnId = getTransactionIdOrThrow(request); auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request); TransactionDetail txn = updateTransaction(ds, txnId, fullPayload.payload, fullPayload.files); string jsonStr = serializeToJson(txn); response.writeBodyString(jsonStr, "application/json"); } @PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/transactions/:transactionId:ulong") void handleDeleteTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); ulong txnId = getTransactionIdOrThrow(request); deleteTransaction(ds, txnId); } @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transaction-tags") void handleGetAllTags(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); string[] tags = ds.getTransactionTagRepository().findAll(); writeJsonBody(response, tags); } private ulong getTransactionIdOrThrow(in ServerHttpRequest request) { return getPathParamOrThrow!ulong(request, "transactionId"); } // Vendors API @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors") void handleGetVendors(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); TransactionVendor[] vendors = getAllVendors(ds); writeJsonBody(response, vendors); } @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong") void handleGetVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); TransactionVendor vendor = getVendor(ds, getVendorId(request)); writeJsonBody(response, vendor); } struct VendorPayload { string name; string description; } @PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/vendors") void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) { VendorPayload payload = readJsonBodyAs!VendorPayload(request); ProfileDataSource ds = getProfileDataSource(request); TransactionVendor vendor = createVendor(ds, payload); writeJsonBody(response, vendor); } @PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong") void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) { VendorPayload payload = readJsonBodyAs!VendorPayload(request); ProfileDataSource ds = getProfileDataSource(request); TransactionVendor updated = updateVendor(ds, getVendorId(request), payload); writeJsonBody(response, updated); } @PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong") void handleDeleteVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); deleteVendor(ds, getVendorId(request)); } private ulong getVendorId(in ServerHttpRequest request) { return getPathParamOrThrow!ulong(request, "vendorId"); } // Categories API @GetMapping(PROFILE_PATH ~ "/categories") void handleGetCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) { TransactionCategoryTree[] categories = getCategories(getProfileDataSource(request)); writeJsonBody(response, categories); } @GetMapping(PROFILE_PATH ~ "/categories/:categoryId:ulong") void handleGetCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) { auto category = getCategory(getProfileDataSource(request), getCategoryId(request)); writeJsonBody(response, category); } @GetMapping(PROFILE_PATH ~ "/categories/:categoryId:ulong/children") void handleGetChildCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) { auto children = getChildCategories(getProfileDataSource(request), getCategoryId(request)); writeJsonBody(response, children); } @GetMapping(PROFILE_PATH ~ "/categories/:categoryId:ulong/balances") void handleGetCategoryBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) { response.status = HttpStatus.NOT_IMPLEMENTED; // TODO: Add an API endpoint to provide a "balance" for the category. // This would be the sum of credits and debits for all transactions set // to this category or any child of it, over a specified interval, or for // some default interval if none is provided. } struct CategoryPayload { string name; string description; string color; Nullable!ulong parentId; } @PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/categories") void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) { CategoryPayload payload = readJsonBodyAs!CategoryPayload(request); ProfileDataSource ds = getProfileDataSource(request); auto category = createCategory(ds, payload); writeJsonBody(response, category); } @PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/categories/:categoryId:ulong") void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) { CategoryPayload payload = readJsonBodyAs!CategoryPayload(request); ProfileDataSource ds = getProfileDataSource(request); ulong categoryId = getCategoryId(request); auto category = updateCategory(ds, categoryId, payload); writeJsonBody(response, category); } @PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/categories/:categoryId:ulong") void handleDeleteCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) { ProfileDataSource ds = getProfileDataSource(request); ulong categoryId = getCategoryId(request); deleteCategory(ds, categoryId); } private ulong getCategoryId(in ServerHttpRequest request) { return getPathParamOrThrow!ulong(request, "categoryId"); }