New API endpoint to count SMS parts and total cost for a service#2812
New API endpoint to count SMS parts and total cost for a service#2812
Conversation
Previously `dao_fetch_sms_cost_for_service_in_range` was only fetching data from the `ft_billing` table which does not include data for today. Now if the `end_date` parameter includes today, then we query the `notifications` table using `sms_total_carrier_fee` and `sms_total_message_price` to calculate today's sms costs
Previously `dao_fetch_sms_cost_for_service_in_range` was only fetching data from the `ft_billing` table which does not include data for today. Now if the `end_date` parameter includes today, then we query the `notifications` table using `sms_total_carrier_fee` and `sms_total_message_price` to calculate today's sms costs
There was a problem hiding this comment.
Pull request overview
Adds a new billing API endpoint to return total SMS fragments and total SMS cost for a service over a requested date range, using ft_billing for historical days and notifications for “today”.
Changes:
- Adds
GET /service/<uuid:service_id>/billing/sms-costwithstart_date/end_datequery validation. - Introduces DAO aggregation
dao_fetch_sms_cost_for_service_in_range(...)combiningft_billing+ same-daynotificationscosts. - Adds unit tests for the DAO and the new REST endpoint.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
app/billing/billing_schemas.py |
Adds JSON schema for start_date/end_date query params. |
app/dao/fact_billing_dao.py |
Implements SMS cost + fragment aggregation across ft_billing and notifications. |
app/billing/rest.py |
Exposes the new /billing/sms-cost route and wires it to the DAO. |
tests/app/dao/test_ft_billing_dao.py |
Adds DAO-level tests for historical/today/combined cases and filters. |
tests/app/billing/test_billing.py |
Adds endpoint-level tests for success and validation failures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| assert json_resp["start_date"] == "2024-06-01" | ||
| assert json_resp["end_date"] == "2024-06-30" | ||
| assert json_resp["fragment_count"] == 15 | ||
| assert json_resp["total_cost"] == 15 * 0.0162 |
There was a problem hiding this comment.
This assertion compares floats computed via 15 * 0.0162, which can be sensitive to float representation/rounding (and may vary depending on how the DB value is stored/returned). To avoid flaky tests, assert using pytest.approx(...) or compare using Decimal (eg serialize total_cost consistently and compare as a string/Decimal).
| assert json_resp["total_cost"] == 15 * 0.0162 | |
| assert json_resp["total_cost"] == pytest.approx(15 * 0.0162) |
| end_date = date.fromisoformat(data["end_date"]) | ||
|
|
||
| if start_date > end_date: | ||
| raise InvalidRequest("start_date must be before end_date", 400) |
There was a problem hiding this comment.
The validation error message says “start_date must be before end_date”, but the code only rejects start_date > end_date (same-day ranges are allowed). Consider changing the message to reflect the actual constraint (eg “start_date must be on or before end_date”) to avoid confusing API consumers.
| raise InvalidRequest("start_date must be before end_date", 400) | |
| raise InvalidRequest("start_date must be on or before end_date", 400) |
| fact_end = min(end_date, today - timedelta(days=1)) | ||
| fact_result = ( | ||
| db.session.query( | ||
| func.coalesce(func.sum(FactBilling.billable_units), 0).label("fragment_count"), |
There was a problem hiding this comment.
fragment_count for historical ft_billing rows is currently computed as sum(FactBilling.billable_units), but elsewhere in this module SMS “billable units/fragments” are consistently billable_units * rate_multiplier (see fetch_sms_free_allowance_remainder and fetch_sms_billing_for_all_services). As written, this endpoint undercounts fragments for any row with rate_multiplier != 1 and makes fragment_count inconsistent with the total_cost calculation (which does apply the multiplier). Update the query to sum FactBilling.billable_units * FactBilling.rate_multiplier (with appropriate coalesce) and adjust downstream tests accordingly.
| func.coalesce(func.sum(FactBilling.billable_units), 0).label("fragment_count"), | |
| func.coalesce( | |
| func.sum(FactBilling.billable_units * func.coalesce(FactBilling.rate_multiplier, 1)), 0 | |
| ).label("fragment_count"), |
| total_cost += Decimal(str(fact_result.total_cost)) | ||
|
|
||
| # Current-day data from Notification table | ||
| if end_date >= today: |
There was a problem hiding this comment.
The Notification-table branch runs whenever end_date >= today, but it doesn’t check start_date. For a future-only range (eg start_date tomorrow, end_date tomorrow), this will still include today’s notifications because it always queries [today_start, today_end). Gate this branch on start_date <= today <= end_date (or start_date <= today in addition to the existing end_date >= today) so results don’t include out-of-range data.
| if end_date >= today: | |
| if start_date <= today and end_date >= today: |
| # fragment_count = sum of billable_units (no multiplier) = 3 + 5 = 8 | ||
| assert result["fragment_count"] == 8 |
There was a problem hiding this comment.
This test documents/locks in the current behavior that fragment_count ignores rate_multiplier (expecting 3 + 5 = 8). If fragment_count is intended to represent SMS fragments/billable units (consistent with other billing queries), the expectation should include the multiplier (31 + 52 = 13) and the comment should be updated accordingly.
| # fragment_count = sum of billable_units (no multiplier) = 3 + 5 = 8 | |
| assert result["fragment_count"] == 8 | |
| # fragment_count = sum of multiplier-adjusted billable_units = (3 * 1) + (5 * 2) = 13 | |
| assert result["fragment_count"] == 13 |
Summary | Résumé
This PR is a simple example of the
rest-endpoint-builderagent adding a new API endpoint/service/<id>/billing/sms-costwhich tallies up thefragment_countandtotal_costfor a service within a given date range.Example output:

Promp & chat
I have a good understanding of the existing patterns. Let me check the
FactBillingmodel to understand the columns available for the SMS cost query, and also readdefinitions.pyfor shared definitions.Now I have full context. The FactBilling model has
service_id,bst_date,notification_type,billable_units,rate_multiplier,rate, andbilling_totalcolumns.I'll add:
rest.pyNow let me add test stubs. Let me check how the existing tests import and structure things:
Now I'll append tests for the new endpoint at the end of the test file:
The import ordering in
rest.pyneeds fixing —from datetime import dateshould come before Flask imports per standard Python import ordering (stdlib before third-party).All changes are complete. Here's a summary of what was added:
Edited files:
billing_schemas.py— added get_sms_cost_for_service_schema requiring start_date and end_date string properties with "format": "date"fact_billing_dao.py— added dao_fetch_sms_cost_for_service_in_range(service_id, start_date, end_date) that queries FactBilling for SMS-type rows, returning total fragment_count (billable_units × rate_multiplier) and total_cost (fragments × rate)rest.py— added GET /service/uuid:service_id/billing/sms-cost?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD route that validates params, checks start < end, calls the DAO, and returns {start_date, end_date, fragment_count, total_cost}test_billing.py— added TestGetSmsCostForService class with 4 tests: happy path with data, zero-result case, missing dates (400), andstart_dateafterend_date(400)Checklist:
billing_schemas.py— edited (addedget_sms_cost_for_service_schema)fact_billing_dao.py— edited (addeddao_fetch_sms_cost_for_service_in_range)rest.py— edited (added new route + updated imports)test_billing.py— edited (added test class)__init__.pychange needed — thebilling_blueprintis already registeredTest instructions | Instructions pour tester la modification
TODO: Fill in test instructions for the reviewer.
Release Instructions | Instructions pour le déploiement
None.
Reviewer checklist | Liste de vérification du réviseur