2022-02-09 17:44:00 +00:00
|
|
|
from datetime import date, datetime, timedelta
|
2018-04-25 14:24:47 +01:00
|
|
|
|
2018-05-16 12:21:59 +01:00
|
|
|
from flask import current_app
|
2022-02-09 17:44:00 +00:00
|
|
|
from notifications_utils.timezones import convert_utc_to_bst
|
2022-03-09 11:55:47 +00:00
|
|
|
from sqlalchemy import Date, Integer, and_, desc, func
|
2018-05-15 11:21:10 +01:00
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
2020-07-10 17:43:40 +01:00
|
|
|
from sqlalchemy.sql.expression import case, literal
|
2018-04-06 11:55:49 +01:00
|
|
|
|
|
|
|
|
from app import db
|
2019-08-06 13:29:59 +01:00
|
|
|
from app.dao.date_util import (
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
get_financial_year_dates,
|
2021-03-10 13:55:06 +00:00
|
|
|
get_financial_year_for_datetime,
|
2019-08-06 13:29:59 +01:00
|
|
|
)
|
2020-02-24 14:19:12 +00:00
|
|
|
from app.dao.organisation_dao import dao_get_organisation_live_services
|
2018-04-24 17:37:04 +01:00
|
|
|
from app.models import (
|
2021-03-10 13:55:06 +00:00
|
|
|
EMAIL_TYPE,
|
|
|
|
|
INTERNATIONAL_POSTAGE_TYPES,
|
2018-04-24 17:37:04 +01:00
|
|
|
KEY_TYPE_TEST,
|
|
|
|
|
LETTER_TYPE,
|
2021-03-10 13:55:06 +00:00
|
|
|
NOTIFICATION_STATUS_TYPES_BILLABLE_FOR_LETTERS,
|
2020-02-19 10:58:06 +00:00
|
|
|
NOTIFICATION_STATUS_TYPES_BILLABLE_SMS,
|
|
|
|
|
NOTIFICATION_STATUS_TYPES_SENT_EMAILS,
|
2021-03-10 13:55:06 +00:00
|
|
|
SMS_TYPE,
|
2019-08-06 13:29:59 +01:00
|
|
|
AnnualBilling,
|
2021-03-10 13:55:06 +00:00
|
|
|
FactBilling,
|
|
|
|
|
LetterRate,
|
|
|
|
|
NotificationHistory,
|
2019-08-06 13:29:59 +01:00
|
|
|
Organisation,
|
2021-03-10 13:55:06 +00:00
|
|
|
Rate,
|
|
|
|
|
Service,
|
2018-04-24 17:37:04 +01:00
|
|
|
)
|
2019-08-21 16:13:04 +01:00
|
|
|
from app.utils import get_london_midnight_in_utc, get_notification_table_to_use
|
2018-04-09 11:38:00 +01:00
|
|
|
|
|
|
|
|
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
def fetch_sms_free_allowance_remainder_until_date(end_date):
|
2019-08-06 13:29:59 +01:00
|
|
|
# ASSUMPTION: AnnualBilling has been populated for year.
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
billing_year = get_financial_year_for_datetime(end_date)
|
2019-08-30 17:16:43 +01:00
|
|
|
start_of_year = date(billing_year, 4, 1)
|
2019-08-29 17:55:50 +01:00
|
|
|
|
|
|
|
|
billable_units = func.coalesce(func.sum(FactBilling.billable_units * FactBilling.rate_multiplier), 0)
|
|
|
|
|
|
2019-08-06 13:29:59 +01:00
|
|
|
query = db.session.query(
|
2019-08-29 17:55:50 +01:00
|
|
|
AnnualBilling.service_id.label("service_id"),
|
2019-08-06 13:29:59 +01:00
|
|
|
AnnualBilling.free_sms_fragment_limit,
|
2019-08-29 17:55:50 +01:00
|
|
|
billable_units.label('billable_units'),
|
|
|
|
|
func.greatest((AnnualBilling.free_sms_fragment_limit - billable_units).cast(Integer), 0).label('sms_remainder')
|
|
|
|
|
).outerjoin(
|
|
|
|
|
# if there are no ft_billing rows for a service we still want to return the annual billing so we can use the
|
|
|
|
|
# free_sms_fragment_limit)
|
|
|
|
|
FactBilling, and_(
|
|
|
|
|
AnnualBilling.service_id == FactBilling.service_id,
|
|
|
|
|
FactBilling.bst_date >= start_of_year,
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
FactBilling.bst_date < end_date,
|
2019-08-29 17:55:50 +01:00
|
|
|
FactBilling.notification_type == SMS_TYPE,
|
|
|
|
|
)
|
2019-08-06 13:29:59 +01:00
|
|
|
).filter(
|
|
|
|
|
AnnualBilling.financial_year_start == billing_year,
|
|
|
|
|
).group_by(
|
2019-08-29 17:55:50 +01:00
|
|
|
AnnualBilling.service_id,
|
2019-08-06 13:29:59 +01:00
|
|
|
AnnualBilling.free_sms_fragment_limit,
|
|
|
|
|
)
|
|
|
|
|
return query
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_sms_billing_for_all_services(start_date, end_date):
|
|
|
|
|
|
|
|
|
|
# ASSUMPTION: AnnualBilling has been populated for year.
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query = fetch_sms_free_allowance_remainder_until_date(start_date).subquery()
|
2019-08-28 17:28:14 +01:00
|
|
|
|
2019-08-06 13:29:59 +01:00
|
|
|
sms_billable_units = func.sum(FactBilling.billable_units * FactBilling.rate_multiplier)
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
|
|
|
|
|
# subtract sms_billable_units units accrued since report's start date to get up-to-date
|
|
|
|
|
# allowance remainder
|
2021-12-14 17:36:03 +00:00
|
|
|
sms_allowance_left = func.greatest(allowance_left_at_start_date_query.c.sms_remainder - sms_billable_units, 0)
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
|
|
|
|
|
# billable units here are for period between start date and end date only, so to see
|
|
|
|
|
# how many are chargeable, we need to see how much free allowance was used up in the
|
|
|
|
|
# period up until report's start date and then do a subtraction
|
2021-12-14 17:36:03 +00:00
|
|
|
chargeable_sms = func.greatest(sms_billable_units - allowance_left_at_start_date_query.c.sms_remainder, 0)
|
2019-08-28 17:28:14 +01:00
|
|
|
sms_cost = chargeable_sms * FactBilling.rate
|
2019-08-06 13:29:59 +01:00
|
|
|
|
|
|
|
|
query = db.session.query(
|
|
|
|
|
Organisation.name.label('organisation_name'),
|
|
|
|
|
Organisation.id.label('organisation_id'),
|
|
|
|
|
Service.name.label("service_name"),
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.id.label("service_id"),
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query.c.free_sms_fragment_limit,
|
2019-08-06 13:29:59 +01:00
|
|
|
FactBilling.rate.label('sms_rate'),
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
sms_allowance_left.label("sms_remainder"),
|
2019-08-06 13:29:59 +01:00
|
|
|
sms_billable_units.label('sms_billable_units'),
|
|
|
|
|
chargeable_sms.label("chargeable_billable_sms"),
|
|
|
|
|
sms_cost.label('sms_cost'),
|
2019-08-29 17:59:31 +01:00
|
|
|
).select_from(
|
|
|
|
|
Service
|
2019-08-06 13:29:59 +01:00
|
|
|
).outerjoin(
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query, Service.id == allowance_left_at_start_date_query.c.service_id
|
2019-08-06 13:29:59 +01:00
|
|
|
).outerjoin(
|
2019-08-29 17:59:31 +01:00
|
|
|
Service.organisation
|
|
|
|
|
).join(
|
|
|
|
|
FactBilling, FactBilling.service_id == Service.id,
|
2019-08-06 13:29:59 +01:00
|
|
|
).filter(
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date,
|
|
|
|
|
FactBilling.notification_type == SMS_TYPE,
|
|
|
|
|
).group_by(
|
|
|
|
|
Organisation.name,
|
|
|
|
|
Organisation.id,
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.id,
|
2019-08-06 13:29:59 +01:00
|
|
|
Service.name,
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query.c.free_sms_fragment_limit,
|
|
|
|
|
allowance_left_at_start_date_query.c.sms_remainder,
|
2019-08-06 13:29:59 +01:00
|
|
|
FactBilling.rate,
|
|
|
|
|
).order_by(
|
|
|
|
|
Organisation.name,
|
|
|
|
|
Service.name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
2021-06-11 11:11:47 +01:00
|
|
|
def fetch_letter_costs_and_totals_for_all_services(start_date, end_date):
|
2019-08-06 13:29:59 +01:00
|
|
|
query = db.session.query(
|
|
|
|
|
Organisation.name.label("organisation_name"),
|
|
|
|
|
Organisation.id.label("organisation_id"),
|
|
|
|
|
Service.name.label("service_name"),
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.id.label("service_id"),
|
2021-06-11 11:11:47 +01:00
|
|
|
func.sum(FactBilling.notifications_sent).label("total_letters"),
|
2019-08-06 13:29:59 +01:00
|
|
|
func.sum(FactBilling.notifications_sent * FactBilling.rate).label("letter_cost")
|
2019-08-30 12:16:12 +01:00
|
|
|
).select_from(
|
|
|
|
|
Service
|
2019-08-06 13:29:59 +01:00
|
|
|
).outerjoin(
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.organisation
|
|
|
|
|
).join(
|
|
|
|
|
FactBilling, FactBilling.service_id == Service.id,
|
2019-08-06 13:29:59 +01:00
|
|
|
).filter(
|
|
|
|
|
FactBilling.service_id == Service.id,
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date,
|
|
|
|
|
FactBilling.notification_type == LETTER_TYPE,
|
|
|
|
|
).group_by(
|
|
|
|
|
Organisation.name,
|
|
|
|
|
Organisation.id,
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.id,
|
2019-08-06 13:29:59 +01:00
|
|
|
Service.name,
|
|
|
|
|
).order_by(
|
|
|
|
|
Organisation.name,
|
|
|
|
|
Service.name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_letter_line_items_for_all_services(start_date, end_date):
|
2020-07-10 17:43:40 +01:00
|
|
|
formatted_postage = case(
|
|
|
|
|
[(FactBilling.postage.in_(INTERNATIONAL_POSTAGE_TYPES), "international")], else_=FactBilling.postage
|
|
|
|
|
).label("postage")
|
|
|
|
|
|
2021-04-26 11:50:17 +01:00
|
|
|
postage_order = case(
|
|
|
|
|
(formatted_postage == "second", 1),
|
|
|
|
|
(formatted_postage == "first", 2),
|
|
|
|
|
(formatted_postage == "international", 3),
|
|
|
|
|
else_=0 # assumes never get 0 as a result
|
|
|
|
|
)
|
2020-07-10 17:43:40 +01:00
|
|
|
|
2019-08-06 13:29:59 +01:00
|
|
|
query = db.session.query(
|
|
|
|
|
Organisation.name.label("organisation_name"),
|
|
|
|
|
Organisation.id.label("organisation_id"),
|
|
|
|
|
Service.name.label("service_name"),
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.id.label("service_id"),
|
2019-08-06 13:29:59 +01:00
|
|
|
FactBilling.rate.label("letter_rate"),
|
2020-07-10 17:43:40 +01:00
|
|
|
formatted_postage,
|
2019-08-06 13:29:59 +01:00
|
|
|
func.sum(FactBilling.notifications_sent).label("letters_sent"),
|
2019-08-30 12:16:12 +01:00
|
|
|
).select_from(
|
|
|
|
|
Service
|
2019-08-06 13:29:59 +01:00
|
|
|
).outerjoin(
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.organisation
|
|
|
|
|
).join(
|
|
|
|
|
FactBilling, FactBilling.service_id == Service.id,
|
2019-08-06 13:29:59 +01:00
|
|
|
).filter(
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date,
|
|
|
|
|
FactBilling.notification_type == LETTER_TYPE,
|
2019-08-19 14:44:08 +01:00
|
|
|
).group_by(
|
2019-08-06 13:29:59 +01:00
|
|
|
Organisation.name,
|
|
|
|
|
Organisation.id,
|
2019-08-30 12:16:12 +01:00
|
|
|
Service.id,
|
2019-08-06 13:29:59 +01:00
|
|
|
Service.name,
|
|
|
|
|
FactBilling.rate,
|
2020-07-10 17:43:40 +01:00
|
|
|
formatted_postage
|
2019-08-06 13:29:59 +01:00
|
|
|
).order_by(
|
|
|
|
|
Organisation.name,
|
|
|
|
|
Service.name,
|
2020-07-10 17:43:40 +01:00
|
|
|
postage_order,
|
2019-08-19 14:23:09 +01:00
|
|
|
FactBilling.rate,
|
2019-08-06 13:29:59 +01:00
|
|
|
)
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
2018-05-11 16:25:16 +01:00
|
|
|
def fetch_billing_totals_for_year(service_id, year):
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
year_start, year_end = get_financial_year_dates(year)
|
2018-05-16 12:21:59 +01:00
|
|
|
"""
|
|
|
|
|
Billing for email: only record the total number of emails.
|
2018-05-16 13:18:36 +01:00
|
|
|
Billing for letters: The billing units is used to fetch the correct rate for the sheet count of the letter.
|
|
|
|
|
Total cost is notifications_sent * rate.
|
2018-05-16 12:21:59 +01:00
|
|
|
Rate multiplier does not apply to email or letters.
|
|
|
|
|
"""
|
|
|
|
|
email_and_letters = db.session.query(
|
|
|
|
|
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
|
|
|
|
|
func.sum(FactBilling.notifications_sent).label("billable_units"),
|
|
|
|
|
FactBilling.rate.label('rate'),
|
|
|
|
|
FactBilling.notification_type.label('notification_type')
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.service_id == service_id,
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
FactBilling.bst_date >= year_start,
|
|
|
|
|
FactBilling.bst_date <= year_end,
|
2018-05-16 12:21:59 +01:00
|
|
|
FactBilling.notification_type.in_([EMAIL_TYPE, LETTER_TYPE])
|
|
|
|
|
).group_by(
|
|
|
|
|
FactBilling.rate,
|
|
|
|
|
FactBilling.notification_type
|
|
|
|
|
)
|
|
|
|
|
"""
|
|
|
|
|
Billing for SMS using the billing_units * rate_multiplier. Billing unit of SMS is the fragment count of a message
|
|
|
|
|
"""
|
|
|
|
|
sms = db.session.query(
|
2018-05-11 16:25:16 +01:00
|
|
|
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
|
|
|
|
|
func.sum(FactBilling.billable_units * FactBilling.rate_multiplier).label("billable_units"),
|
|
|
|
|
FactBilling.rate,
|
|
|
|
|
FactBilling.notification_type
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.service_id == service_id,
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
FactBilling.bst_date >= year_start,
|
|
|
|
|
FactBilling.bst_date <= year_end,
|
2018-05-16 12:21:59 +01:00
|
|
|
FactBilling.notification_type == SMS_TYPE
|
2018-05-11 16:25:16 +01:00
|
|
|
).group_by(
|
|
|
|
|
FactBilling.rate,
|
|
|
|
|
FactBilling.notification_type
|
2018-05-16 12:21:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
yearly_data = email_and_letters.union_all(sms).order_by(
|
|
|
|
|
'notification_type',
|
|
|
|
|
'rate'
|
2018-05-11 16:25:16 +01:00
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
return yearly_data
|
|
|
|
|
|
|
|
|
|
|
2018-04-27 15:15:55 +01:00
|
|
|
def fetch_monthly_billing_for_year(service_id, year):
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
year_start, year_end = get_financial_year_dates(year)
|
2019-08-21 16:13:04 +01:00
|
|
|
today = convert_utc_to_bst(datetime.utcnow()).date()
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
|
2018-04-09 11:38:00 +01:00
|
|
|
# if year end date is less than today, we are calculating for data in the past and have no need for deltas.
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
if year_end >= today:
|
2020-02-26 17:38:20 +00:00
|
|
|
data = fetch_billing_data_for_day(process_day=today, service_id=service_id, check_permissions=True)
|
|
|
|
|
for d in data:
|
|
|
|
|
update_fact_billing(data=d, process_day=today)
|
2018-04-25 14:24:47 +01:00
|
|
|
|
2018-05-16 12:21:59 +01:00
|
|
|
email_and_letters = db.session.query(
|
|
|
|
|
func.date_trunc('month', FactBilling.bst_date).cast(Date).label("month"),
|
|
|
|
|
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
|
|
|
|
|
func.sum(FactBilling.notifications_sent).label("billable_units"),
|
|
|
|
|
FactBilling.rate.label('rate'),
|
2018-09-27 13:58:01 +01:00
|
|
|
FactBilling.notification_type.label('notification_type'),
|
|
|
|
|
FactBilling.postage
|
2018-05-16 12:21:59 +01:00
|
|
|
).filter(
|
|
|
|
|
FactBilling.service_id == service_id,
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
FactBilling.bst_date >= year_start,
|
|
|
|
|
FactBilling.bst_date <= year_end,
|
2018-05-16 12:21:59 +01:00
|
|
|
FactBilling.notification_type.in_([EMAIL_TYPE, LETTER_TYPE])
|
|
|
|
|
).group_by(
|
|
|
|
|
'month',
|
|
|
|
|
FactBilling.rate,
|
2018-09-27 13:58:01 +01:00
|
|
|
FactBilling.notification_type,
|
|
|
|
|
FactBilling.postage
|
2018-05-16 12:21:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
sms = db.session.query(
|
2018-05-08 13:53:44 +01:00
|
|
|
func.date_trunc('month', FactBilling.bst_date).cast(Date).label("month"),
|
2018-04-24 17:37:04 +01:00
|
|
|
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
|
2018-05-04 13:09:14 +01:00
|
|
|
func.sum(FactBilling.billable_units * FactBilling.rate_multiplier).label("billable_units"),
|
2018-04-09 11:38:00 +01:00
|
|
|
FactBilling.rate,
|
2018-09-27 13:58:01 +01:00
|
|
|
FactBilling.notification_type,
|
|
|
|
|
FactBilling.postage
|
2018-04-09 11:38:00 +01:00
|
|
|
).filter(
|
|
|
|
|
FactBilling.service_id == service_id,
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
FactBilling.bst_date >= year_start,
|
|
|
|
|
FactBilling.bst_date <= year_end,
|
2018-05-16 12:21:59 +01:00
|
|
|
FactBilling.notification_type == SMS_TYPE
|
2018-04-24 17:37:04 +01:00
|
|
|
).group_by(
|
2018-04-30 16:34:40 +01:00
|
|
|
'month',
|
2018-04-24 17:37:04 +01:00
|
|
|
FactBilling.rate,
|
2018-09-27 13:58:01 +01:00
|
|
|
FactBilling.notification_type,
|
|
|
|
|
FactBilling.postage
|
2018-05-16 12:21:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
yearly_data = email_and_letters.union_all(sms).order_by(
|
2018-04-30 16:34:40 +01:00
|
|
|
'month',
|
2018-05-16 12:21:59 +01:00
|
|
|
'notification_type',
|
|
|
|
|
'rate'
|
2018-04-09 11:38:00 +01:00
|
|
|
).all()
|
|
|
|
|
|
2018-04-24 17:37:04 +01:00
|
|
|
return yearly_data
|
2018-07-26 18:41:06 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_billing_data_for_service_for_day(process_day, service_id):
|
|
|
|
|
"""
|
|
|
|
|
Delete all ft_billing data for a given service on a given bst_date
|
|
|
|
|
|
|
|
|
|
Returns how many rows were deleted
|
|
|
|
|
"""
|
|
|
|
|
return FactBilling.query.filter(
|
|
|
|
|
FactBilling.bst_date == process_day,
|
|
|
|
|
FactBilling.service_id == service_id
|
|
|
|
|
).delete()
|
2018-04-24 17:37:04 +01:00
|
|
|
|
|
|
|
|
|
2020-02-19 16:57:04 +00:00
|
|
|
def fetch_billing_data_for_day(process_day, service_id=None, check_permissions=False):
|
2022-02-09 17:44:00 +00:00
|
|
|
start_date = get_london_midnight_in_utc(process_day)
|
|
|
|
|
end_date = get_london_midnight_in_utc(process_day + timedelta(days=1))
|
2018-05-16 12:21:59 +01:00
|
|
|
current_app.logger.info("Populate ft_billing for {} to {}".format(start_date, end_date))
|
2018-10-24 17:07:30 +01:00
|
|
|
transit_data = []
|
2019-07-18 15:29:54 +01:00
|
|
|
if not service_id:
|
2019-08-21 16:13:04 +01:00
|
|
|
services = Service.query.all()
|
2019-07-18 15:29:54 +01:00
|
|
|
else:
|
2019-08-21 16:13:04 +01:00
|
|
|
services = [Service.query.get(service_id)]
|
|
|
|
|
|
|
|
|
|
for service in services:
|
2019-07-18 15:29:54 +01:00
|
|
|
for notification_type in (SMS_TYPE, EMAIL_TYPE, LETTER_TYPE):
|
2020-02-19 16:57:04 +00:00
|
|
|
if (not check_permissions) or service.has_permission(notification_type):
|
|
|
|
|
table = get_notification_table_to_use(service, notification_type, process_day,
|
|
|
|
|
has_delete_task_run=False)
|
|
|
|
|
results = _query_for_billing_data(
|
|
|
|
|
table=table,
|
|
|
|
|
notification_type=notification_type,
|
|
|
|
|
start_date=start_date,
|
|
|
|
|
end_date=end_date,
|
|
|
|
|
service=service
|
|
|
|
|
)
|
|
|
|
|
transit_data += results
|
2018-10-24 17:07:30 +01:00
|
|
|
|
|
|
|
|
return transit_data
|
2018-04-24 17:37:04 +01:00
|
|
|
|
|
|
|
|
|
2020-02-19 10:58:06 +00:00
|
|
|
def _query_for_billing_data(table, notification_type, start_date, end_date, service):
|
|
|
|
|
def _email_query():
|
|
|
|
|
return db.session.query(
|
|
|
|
|
table.template_id,
|
|
|
|
|
literal(service.crown).label('crown'),
|
|
|
|
|
literal(service.id).label('service_id'),
|
|
|
|
|
literal(notification_type).label('notification_type'),
|
|
|
|
|
literal('ses').label('sent_by'),
|
|
|
|
|
literal(0).label('rate_multiplier'),
|
|
|
|
|
literal(False).label('international'),
|
|
|
|
|
literal(None).label('letter_page_count'),
|
|
|
|
|
literal('none').label('postage'),
|
|
|
|
|
literal(0).label('billable_units'),
|
|
|
|
|
func.count().label('notifications_sent'),
|
|
|
|
|
).filter(
|
|
|
|
|
table.status.in_(NOTIFICATION_STATUS_TYPES_SENT_EMAILS),
|
|
|
|
|
table.key_type != KEY_TYPE_TEST,
|
|
|
|
|
table.created_at >= start_date,
|
|
|
|
|
table.created_at < end_date,
|
|
|
|
|
table.notification_type == notification_type,
|
|
|
|
|
table.service_id == service.id
|
|
|
|
|
).group_by(
|
|
|
|
|
table.template_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _sms_query():
|
|
|
|
|
sent_by = func.coalesce(table.sent_by, 'unknown')
|
|
|
|
|
rate_multiplier = func.coalesce(table.rate_multiplier, 1).cast(Integer)
|
|
|
|
|
international = func.coalesce(table.international, False)
|
|
|
|
|
return db.session.query(
|
|
|
|
|
table.template_id,
|
|
|
|
|
literal(service.crown).label('crown'),
|
|
|
|
|
literal(service.id).label('service_id'),
|
|
|
|
|
literal(notification_type).label('notification_type'),
|
|
|
|
|
sent_by.label('sent_by'),
|
|
|
|
|
rate_multiplier.label('rate_multiplier'),
|
|
|
|
|
international.label('international'),
|
|
|
|
|
literal(None).label('letter_page_count'),
|
|
|
|
|
literal('none').label('postage'),
|
|
|
|
|
func.sum(table.billable_units).label('billable_units'),
|
|
|
|
|
func.count().label('notifications_sent'),
|
|
|
|
|
).filter(
|
|
|
|
|
table.status.in_(NOTIFICATION_STATUS_TYPES_BILLABLE_SMS),
|
|
|
|
|
table.key_type != KEY_TYPE_TEST,
|
|
|
|
|
table.created_at >= start_date,
|
|
|
|
|
table.created_at < end_date,
|
|
|
|
|
table.notification_type == notification_type,
|
|
|
|
|
table.service_id == service.id
|
|
|
|
|
).group_by(
|
|
|
|
|
table.template_id,
|
|
|
|
|
sent_by,
|
|
|
|
|
rate_multiplier,
|
|
|
|
|
international,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _letter_query():
|
|
|
|
|
rate_multiplier = func.coalesce(table.rate_multiplier, 1).cast(Integer)
|
|
|
|
|
postage = func.coalesce(table.postage, 'none')
|
|
|
|
|
return db.session.query(
|
|
|
|
|
table.template_id,
|
|
|
|
|
literal(service.crown).label('crown'),
|
|
|
|
|
literal(service.id).label('service_id'),
|
|
|
|
|
literal(notification_type).label('notification_type'),
|
|
|
|
|
literal('dvla').label('sent_by'),
|
|
|
|
|
rate_multiplier.label('rate_multiplier'),
|
2020-08-21 09:19:27 +01:00
|
|
|
table.international,
|
2020-02-19 10:58:06 +00:00
|
|
|
table.billable_units.label('letter_page_count'),
|
|
|
|
|
postage.label('postage'),
|
|
|
|
|
func.sum(table.billable_units).label('billable_units'),
|
|
|
|
|
func.count().label('notifications_sent'),
|
|
|
|
|
).filter(
|
|
|
|
|
table.status.in_(NOTIFICATION_STATUS_TYPES_BILLABLE_FOR_LETTERS),
|
|
|
|
|
table.key_type != KEY_TYPE_TEST,
|
|
|
|
|
table.created_at >= start_date,
|
|
|
|
|
table.created_at < end_date,
|
|
|
|
|
table.notification_type == notification_type,
|
|
|
|
|
table.service_id == service.id
|
|
|
|
|
).group_by(
|
|
|
|
|
table.template_id,
|
|
|
|
|
rate_multiplier,
|
|
|
|
|
table.billable_units,
|
2020-08-21 09:19:27 +01:00
|
|
|
postage,
|
|
|
|
|
table.international
|
2020-02-19 10:58:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
query_funcs = {
|
|
|
|
|
SMS_TYPE: _sms_query,
|
|
|
|
|
EMAIL_TYPE: _email_query,
|
|
|
|
|
LETTER_TYPE: _letter_query
|
2019-07-18 15:29:54 +01:00
|
|
|
}
|
2019-11-15 10:23:48 +00:00
|
|
|
|
2020-02-19 10:58:06 +00:00
|
|
|
query = query_funcs[notification_type]()
|
2019-07-18 15:29:54 +01:00
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
2018-04-24 17:37:04 +01:00
|
|
|
def get_rates_for_billing():
|
2019-04-03 13:07:49 +01:00
|
|
|
non_letter_rates = Rate.query.order_by(desc(Rate.valid_from)).all()
|
|
|
|
|
letter_rates = LetterRate.query.order_by(desc(LetterRate.start_date)).all()
|
2018-04-24 17:37:04 +01:00
|
|
|
return non_letter_rates, letter_rates
|
|
|
|
|
|
|
|
|
|
|
2018-07-23 15:14:37 +01:00
|
|
|
def get_service_ids_that_need_billing_populated(start_date, end_date):
|
|
|
|
|
return db.session.query(
|
|
|
|
|
NotificationHistory.service_id
|
|
|
|
|
).filter(
|
|
|
|
|
NotificationHistory.created_at >= start_date,
|
|
|
|
|
NotificationHistory.created_at <= end_date,
|
|
|
|
|
NotificationHistory.notification_type.in_([SMS_TYPE, EMAIL_TYPE, LETTER_TYPE]),
|
|
|
|
|
NotificationHistory.billable_units != 0
|
|
|
|
|
).distinct().all()
|
|
|
|
|
|
|
|
|
|
|
2018-09-14 17:52:16 +01:00
|
|
|
def get_rate(
|
|
|
|
|
non_letter_rates, letter_rates, notification_type, date, crown=None, letter_page_count=None, post_class='second'
|
|
|
|
|
):
|
2019-04-03 14:52:41 +01:00
|
|
|
start_of_day = get_london_midnight_in_utc(date)
|
|
|
|
|
|
2018-04-24 17:37:04 +01:00
|
|
|
if notification_type == LETTER_TYPE:
|
2018-07-31 11:04:48 +01:00
|
|
|
if letter_page_count == 0:
|
|
|
|
|
return 0
|
2021-03-23 16:07:57 +00:00
|
|
|
# if crown is not set default to true, this is okay because the rates are the same for both crown and non-crown.
|
|
|
|
|
crown = crown or True
|
2018-09-14 17:52:16 +01:00
|
|
|
return next(
|
2019-04-03 13:07:49 +01:00
|
|
|
r.rate
|
|
|
|
|
for r in letter_rates if (
|
2019-04-03 14:52:41 +01:00
|
|
|
start_of_day >= r.start_date and
|
2019-04-03 13:07:49 +01:00
|
|
|
crown == r.crown and
|
|
|
|
|
letter_page_count == r.sheet_count and
|
|
|
|
|
post_class == r.post_class
|
|
|
|
|
)
|
2018-09-14 17:52:16 +01:00
|
|
|
)
|
2018-04-24 17:37:04 +01:00
|
|
|
elif notification_type == SMS_TYPE:
|
2019-04-03 13:07:49 +01:00
|
|
|
return next(
|
|
|
|
|
r.rate
|
|
|
|
|
for r in non_letter_rates if (
|
|
|
|
|
notification_type == r.notification_type and
|
2019-04-03 14:52:41 +01:00
|
|
|
start_of_day >= r.valid_from
|
2019-04-03 13:07:49 +01:00
|
|
|
)
|
|
|
|
|
)
|
2018-04-24 17:37:04 +01:00
|
|
|
else:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
2018-04-25 14:24:47 +01:00
|
|
|
def update_fact_billing(data, process_day):
|
2018-04-24 17:37:04 +01:00
|
|
|
non_letter_rates, letter_rates = get_rates_for_billing()
|
2018-05-15 11:21:10 +01:00
|
|
|
rate = get_rate(non_letter_rates,
|
|
|
|
|
letter_rates,
|
|
|
|
|
data.notification_type,
|
|
|
|
|
process_day,
|
|
|
|
|
data.crown,
|
2018-09-14 17:52:16 +01:00
|
|
|
data.letter_page_count,
|
2018-09-28 16:32:18 +01:00
|
|
|
data.postage)
|
2018-05-15 11:21:10 +01:00
|
|
|
billing_record = create_billing_record(data, rate, process_day)
|
2019-07-18 15:29:54 +01:00
|
|
|
|
2018-05-15 11:21:10 +01:00
|
|
|
table = FactBilling.__table__
|
|
|
|
|
'''
|
|
|
|
|
This uses the Postgres upsert to avoid race conditions when two threads try to insert
|
|
|
|
|
at the same row. The excluded object refers to values that we tried to insert but were
|
|
|
|
|
rejected.
|
|
|
|
|
http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#insert-on-conflict-upsert
|
|
|
|
|
'''
|
|
|
|
|
stmt = insert(table).values(
|
|
|
|
|
bst_date=billing_record.bst_date,
|
|
|
|
|
template_id=billing_record.template_id,
|
|
|
|
|
service_id=billing_record.service_id,
|
|
|
|
|
provider=billing_record.provider,
|
|
|
|
|
rate_multiplier=billing_record.rate_multiplier,
|
|
|
|
|
notification_type=billing_record.notification_type,
|
|
|
|
|
international=billing_record.international,
|
|
|
|
|
billable_units=billing_record.billable_units,
|
|
|
|
|
notifications_sent=billing_record.notifications_sent,
|
2018-09-26 11:28:59 +01:00
|
|
|
rate=billing_record.rate,
|
|
|
|
|
postage=billing_record.postage,
|
2018-05-15 11:21:10 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
stmt = stmt.on_conflict_do_update(
|
2018-05-21 14:38:25 +01:00
|
|
|
constraint="ft_billing_pkey",
|
2018-05-15 11:21:10 +01:00
|
|
|
set_={"notifications_sent": stmt.excluded.notifications_sent,
|
2018-05-22 14:49:48 +01:00
|
|
|
"billable_units": stmt.excluded.billable_units,
|
|
|
|
|
"updated_at": datetime.utcnow()
|
2018-05-15 11:21:10 +01:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
db.session.connection().execute(stmt)
|
2018-04-24 17:37:04 +01:00
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_billing_record(data, rate, process_day):
|
|
|
|
|
billing_record = FactBilling(
|
2019-04-02 15:15:07 +01:00
|
|
|
bst_date=process_day,
|
2018-04-24 17:37:04 +01:00
|
|
|
template_id=data.template_id,
|
|
|
|
|
service_id=data.service_id,
|
|
|
|
|
notification_type=data.notification_type,
|
|
|
|
|
provider=data.sent_by,
|
|
|
|
|
rate_multiplier=data.rate_multiplier,
|
|
|
|
|
international=data.international,
|
|
|
|
|
billable_units=data.billable_units,
|
|
|
|
|
notifications_sent=data.notifications_sent,
|
2018-09-26 11:28:59 +01:00
|
|
|
rate=rate,
|
|
|
|
|
postage=data.postage,
|
2018-04-24 17:37:04 +01:00
|
|
|
)
|
|
|
|
|
return billing_record
|
2020-02-24 14:19:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_letter_costs_for_organisation(organisation_id, start_date, end_date):
|
|
|
|
|
query = db.session.query(
|
|
|
|
|
Service.name.label("service_name"),
|
|
|
|
|
Service.id.label("service_id"),
|
|
|
|
|
func.sum(FactBilling.notifications_sent * FactBilling.rate).label("letter_cost")
|
|
|
|
|
).select_from(
|
|
|
|
|
Service
|
|
|
|
|
).join(
|
|
|
|
|
FactBilling, FactBilling.service_id == Service.id,
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date,
|
|
|
|
|
FactBilling.notification_type == LETTER_TYPE,
|
2020-02-27 13:52:02 +00:00
|
|
|
Service.organisation_id == organisation_id,
|
|
|
|
|
Service.restricted.is_(False)
|
2020-02-24 14:19:12 +00:00
|
|
|
).group_by(
|
|
|
|
|
Service.id,
|
|
|
|
|
Service.name,
|
|
|
|
|
).order_by(
|
|
|
|
|
Service.name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_email_usage_for_organisation(organisation_id, start_date, end_date):
|
|
|
|
|
query = db.session.query(
|
|
|
|
|
Service.name.label("service_name"),
|
|
|
|
|
Service.id.label("service_id"),
|
|
|
|
|
func.sum(FactBilling.notifications_sent).label("emails_sent")
|
|
|
|
|
).select_from(
|
|
|
|
|
Service
|
|
|
|
|
).join(
|
|
|
|
|
FactBilling, FactBilling.service_id == Service.id,
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date,
|
|
|
|
|
FactBilling.notification_type == EMAIL_TYPE,
|
2020-02-27 13:52:02 +00:00
|
|
|
Service.organisation_id == organisation_id,
|
|
|
|
|
Service.restricted.is_(False)
|
2020-02-24 14:19:12 +00:00
|
|
|
).group_by(
|
|
|
|
|
Service.id,
|
|
|
|
|
Service.name,
|
|
|
|
|
).order_by(
|
|
|
|
|
Service.name
|
|
|
|
|
)
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_sms_billing_for_organisation(organisation_id, start_date, end_date):
|
|
|
|
|
# ASSUMPTION: AnnualBilling has been populated for year.
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query = fetch_sms_free_allowance_remainder_until_date(start_date).subquery()
|
2020-02-24 14:19:12 +00:00
|
|
|
|
2022-01-11 08:46:46 +00:00
|
|
|
sms_billable_units = func.coalesce(func.sum(FactBilling.billable_units * FactBilling.rate_multiplier), 0)
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
|
|
|
|
|
# subtract sms_billable_units units accrued since report's start date to get up-to-date
|
|
|
|
|
# allowance remainder
|
2021-12-14 17:36:03 +00:00
|
|
|
sms_allowance_left = func.greatest(allowance_left_at_start_date_query.c.sms_remainder - sms_billable_units, 0)
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
|
|
|
|
|
# billable units here are for period between start date and end date only, so to see
|
|
|
|
|
# how many are chargeable, we need to see how much free allowance was used up in the
|
|
|
|
|
# period up until report's start date and then do a subtraction
|
2021-12-14 17:36:03 +00:00
|
|
|
chargeable_sms = func.greatest(sms_billable_units - allowance_left_at_start_date_query.c.sms_remainder, 0)
|
2020-02-24 14:19:12 +00:00
|
|
|
sms_cost = chargeable_sms * FactBilling.rate
|
|
|
|
|
|
|
|
|
|
query = db.session.query(
|
|
|
|
|
Service.name.label("service_name"),
|
|
|
|
|
Service.id.label("service_id"),
|
2022-01-11 08:46:46 +00:00
|
|
|
func.coalesce(allowance_left_at_start_date_query.c.free_sms_fragment_limit, 0).label('free_sms_fragment_limit'),
|
|
|
|
|
func.coalesce(FactBilling.rate, 0).label('sms_rate'),
|
|
|
|
|
func.coalesce(sms_allowance_left, 0).label("sms_remainder"),
|
|
|
|
|
func.coalesce(sms_billable_units, 0).label('sms_billable_units'),
|
|
|
|
|
func.coalesce(chargeable_sms, 0).label("chargeable_billable_sms"),
|
|
|
|
|
func.coalesce(sms_cost, 0).label('sms_cost'),
|
2021-01-12 09:44:35 +00:00
|
|
|
Service.active.label("active")
|
2020-02-24 14:19:12 +00:00
|
|
|
).select_from(
|
|
|
|
|
Service
|
|
|
|
|
).outerjoin(
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query, Service.id == allowance_left_at_start_date_query.c.service_id
|
2022-01-11 08:46:46 +00:00
|
|
|
).outerjoin(
|
|
|
|
|
FactBilling, and_(
|
|
|
|
|
Service.id == FactBilling.service_id,
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date < end_date,
|
|
|
|
|
FactBilling.notification_type == SMS_TYPE,
|
|
|
|
|
)
|
2020-02-24 14:19:12 +00:00
|
|
|
).filter(
|
2020-02-27 13:52:02 +00:00
|
|
|
Service.organisation_id == organisation_id,
|
|
|
|
|
Service.restricted.is_(False)
|
2020-02-24 14:19:12 +00:00
|
|
|
).group_by(
|
|
|
|
|
Service.id,
|
|
|
|
|
Service.name,
|
Fix calculating remaining free allowance for SMS
The way it was done before, the remainder was incorrect in the
billing report and in the org usage query - it was the sms remainder
left at the start of the report period, not at the end of that period.
This became apparent when we tried to show sms_remainder on the org
usage report, where start date is always the start of the financial year.
We saw that sms sent by services did not reduce their free allowance
remainder according to the report. As a result of this, we had to
temporarily remove of sms_remainder column from the report, until
we fix the bug - it has been fixed now, yay!
I think the bug has snuck in partially because our fixtures for testing
this part of the code are quite complex, so it was
harder to see that numbers don't add up. I have added comments
to the tests to try and make it a bit clearer why the results are
as they are.
I also added comments to the code, and renamed some variables,
to make it easier to understand, as there are quite a few
moving parts in it - subqueries and the like.
I also renamed the fetch_sms_free_allowance_remainder method to
fetch_sms_free_allowance_remainder_until_date so it is clearer
what it does.
2021-12-09 17:50:03 +00:00
|
|
|
allowance_left_at_start_date_query.c.free_sms_fragment_limit,
|
|
|
|
|
allowance_left_at_start_date_query.c.sms_remainder,
|
2020-02-24 14:19:12 +00:00
|
|
|
FactBilling.rate,
|
|
|
|
|
).order_by(
|
|
|
|
|
Service.name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_usage_year_for_organisation(organisation_id, year):
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
year_start, year_end = get_financial_year_dates(year)
|
2020-02-24 14:19:12 +00:00
|
|
|
today = convert_utc_to_bst(datetime.utcnow()).date()
|
|
|
|
|
services = dao_get_organisation_live_services(organisation_id)
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
|
2020-02-24 14:19:12 +00:00
|
|
|
# if year end date is less than today, we are calculating for data in the past and have no need for deltas.
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
if year_end >= today:
|
2020-02-24 14:19:12 +00:00
|
|
|
for service in services:
|
2020-02-26 17:38:20 +00:00
|
|
|
data = fetch_billing_data_for_day(process_day=today, service_id=service.id)
|
|
|
|
|
for d in data:
|
|
|
|
|
update_fact_billing(data=d, process_day=today)
|
2020-02-24 14:19:12 +00:00
|
|
|
service_with_usage = {}
|
|
|
|
|
# initialise results
|
|
|
|
|
for service in services:
|
|
|
|
|
service_with_usage[str(service.id)] = {
|
|
|
|
|
'service_id': service.id,
|
|
|
|
|
'service_name': service.name,
|
|
|
|
|
'free_sms_limit': 0,
|
|
|
|
|
'sms_remainder': 0,
|
|
|
|
|
'sms_billable_units': 0,
|
|
|
|
|
'chargeable_billable_sms': 0,
|
2020-02-25 17:47:03 +00:00
|
|
|
'sms_cost': 0.0,
|
|
|
|
|
'letter_cost': 0.0,
|
2021-01-12 09:44:35 +00:00
|
|
|
'emails_sent': 0,
|
|
|
|
|
'active': service.active
|
2020-02-24 14:19:12 +00:00
|
|
|
}
|
Standardise timezones for service usage APIs
We want to query for service usage in the BST financial year:
2022-04-01T00:00:00+01:00 to 2023-03-31T23:59:59+01:00 =>
2022-04-01 to 2023-03-31 # bst_date
Previously we were only doing this explicitly for the monthly API
and it seemed like the yearly usage API was incorrectly querying:
2022-03-31T23:00:00+00:00 to 2023-03-30T23:00:00+00:00 =>
2022-03-31 to 2023-03-30 # "bst_date"
However, it turns out this isn't a problem for two reasons:
1. We've been lucky that none of our rates have changed since 2017,
which is long ago enough that no one would care.
2. There's a quirk somewhere in Sqlalchemy / Postgres that has been
compensating for the lack of explicit BST conversion.
To help ensure we do this consistently in future I've DRYed-up the
BST conversion into a new utility. I could have just hard-coded the
dates but it seemed strange to have the knowledge twice.
I've also adjusted the tests so they detect if we accidentally use
data from a different financial year. (2) is why none of the test
assertions actually need changing and users won't be affected.
Sqlalchemy / Postgres quirk
===========================
The following queries were run on the same data but results differ:
FactBilling.query.filter(FactBilling.bst_date >= datetime(2021,3,31,23,0), FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 4, 1)
FactBilling.query.filter(FactBilling.bst_date >= '2021-03-31 23:00:00', FactBilling.bst_date <= '2021-04-05').order_by(FactBilling.bst_date).first().bst_date
datetime.date(2021, 3, 31)
Looking at the actual query for the first item above still suggests
the results should be the same, but for the use of "timestamp".
SELECT ...
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type IN ('email', 'letter') GROUP BY ft_billing.rate, ft_billing.notification_type UNION ALL SELECT sum(ft_billing.notifications_sent) AS notifications_sent, sum(ft_billing.billable_units * ft_billing.rate_multiplier) AS billable_units, ft_billing.rate AS ft_billing_rate, ft_billing.notification_type AS ft_billing_notification_type
FROM ft_billing
WHERE ft_billing.service_id = '16b60315-9dab-45d3-a609-e871fbbf5345'::uuid AND ft_billing.bst_date >= '2016-03-31T23:00:00'::timestamp AND ft_billing.bst_date <= '2017-03-31T22:59:59.999999'::timestamp AND ft_billing.notification_type = 'sms' GROUP BY ft_billing.rate, ft_billing.notification_type) AS anon_1 ORDER BY anon_1.notification_type, anon_1.rate
If we try some manual queries with and without '::timestamp' we get:
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00' order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
select distinct(bst_date) from ft_billing where bst_date >= '2022-04-20T23:00:00'::timestamp order by bst_date desc;
bst_date
------------
2022-04-21
2022-04-20
It looks like this is happening because all client connections are
aware of the local timezone, and naive datetimes are interpreted as
being in UTC - not necessarily true, but saves us here!
The monthly API datetimes were pre-converted to dates, so none of
this was relevant for deciding exactly which date to use.
2022-04-21 16:56:28 +01:00
|
|
|
sms_usages = fetch_sms_billing_for_organisation(organisation_id, year_start, year_end)
|
|
|
|
|
letter_usages = fetch_letter_costs_for_organisation(organisation_id, year_start, year_end)
|
|
|
|
|
email_usages = fetch_email_usage_for_organisation(organisation_id, year_start, year_end)
|
2020-02-24 14:19:12 +00:00
|
|
|
for usage in sms_usages:
|
|
|
|
|
service_with_usage[str(usage.service_id)] = {
|
2020-02-24 17:02:25 +00:00
|
|
|
'service_id': usage.service_id,
|
|
|
|
|
'service_name': usage.service_name,
|
|
|
|
|
'free_sms_limit': usage.free_sms_fragment_limit,
|
|
|
|
|
'sms_remainder': usage.sms_remainder,
|
|
|
|
|
'sms_billable_units': usage.sms_billable_units,
|
2020-02-25 17:47:03 +00:00
|
|
|
'chargeable_billable_sms': usage.chargeable_billable_sms,
|
2020-02-25 17:34:03 +00:00
|
|
|
'sms_cost': float(usage.sms_cost),
|
2020-02-25 17:47:03 +00:00
|
|
|
'letter_cost': 0.0,
|
2021-01-12 09:44:35 +00:00
|
|
|
'emails_sent': 0,
|
|
|
|
|
'active': usage.active
|
2020-02-24 14:19:12 +00:00
|
|
|
}
|
|
|
|
|
for letter_usage in letter_usages:
|
2020-02-25 17:34:03 +00:00
|
|
|
service_with_usage[str(letter_usage.service_id)]['letter_cost'] = float(letter_usage.letter_cost)
|
2020-02-24 14:19:12 +00:00
|
|
|
for email_usage in email_usages:
|
|
|
|
|
service_with_usage[str(email_usage.service_id)]['emails_sent'] = email_usage.emails_sent
|
|
|
|
|
|
|
|
|
|
return service_with_usage
|
2021-02-23 18:37:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_billing_details_for_all_services():
|
|
|
|
|
billing_details = db.session.query(
|
|
|
|
|
Service.id.label('service_id'),
|
|
|
|
|
func.coalesce(Service.purchase_order_number, Organisation.purchase_order_number).label('purchase_order_number'),
|
|
|
|
|
func.coalesce(Service.billing_contact_names, Organisation.billing_contact_names).label('billing_contact_names'),
|
|
|
|
|
func.coalesce(
|
|
|
|
|
Service.billing_contact_email_addresses,
|
|
|
|
|
Organisation.billing_contact_email_addresses
|
|
|
|
|
).label('billing_contact_email_addresses'),
|
|
|
|
|
func.coalesce(Service.billing_reference, Organisation.billing_reference).label('billing_reference'),
|
|
|
|
|
).outerjoin(
|
|
|
|
|
Service.organisation
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
return billing_details
|
2022-03-03 14:47:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_daily_volumes_for_platform(start_date, end_date):
|
|
|
|
|
# query to return the total notifications sent per day for each channel. NB start and end dates are inclusive
|
|
|
|
|
|
|
|
|
|
daily_volume_stats = db.session.query(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == SMS_TYPE, FactBilling.notifications_sent)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('sms_totals'),
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == SMS_TYPE, FactBilling.billable_units)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('sms_fragment_totals'),
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == SMS_TYPE, FactBilling.billable_units * FactBilling.rate_multiplier)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('sms_fragments_times_multiplier'),
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == EMAIL_TYPE, FactBilling.notifications_sent)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('email_totals'),
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == LETTER_TYPE, FactBilling.notifications_sent)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('letter_totals'),
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == LETTER_TYPE, FactBilling.billable_units)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('letter_sheet_totals')
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date
|
|
|
|
|
).group_by(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
FactBilling.notification_type
|
|
|
|
|
).subquery()
|
|
|
|
|
|
|
|
|
|
aggregated_totals = db.session.query(
|
|
|
|
|
daily_volume_stats.c.bst_date.cast(db.Text).label('bst_date'),
|
2022-03-09 11:55:47 +00:00
|
|
|
func.sum(daily_volume_stats.c.sms_totals).label('sms_totals'),
|
|
|
|
|
func.sum(daily_volume_stats.c.sms_fragment_totals).label('sms_fragment_totals'),
|
2022-03-03 14:47:56 +00:00
|
|
|
func.sum(
|
2022-03-09 11:55:47 +00:00
|
|
|
daily_volume_stats.c.sms_fragments_times_multiplier).label('sms_chargeable_units'),
|
|
|
|
|
func.sum(daily_volume_stats.c.email_totals).label('email_totals'),
|
|
|
|
|
func.sum(daily_volume_stats.c.letter_totals).label('letter_totals'),
|
|
|
|
|
func.sum(daily_volume_stats.c.letter_sheet_totals).label('letter_sheet_totals')
|
2022-03-03 14:47:56 +00:00
|
|
|
).group_by(
|
|
|
|
|
daily_volume_stats.c.bst_date
|
|
|
|
|
).order_by(
|
|
|
|
|
daily_volume_stats.c.bst_date
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
return aggregated_totals
|
|
|
|
|
|
|
|
|
|
|
2022-04-07 17:52:37 +01:00
|
|
|
def fetch_daily_sms_provider_volumes_for_platform(start_date, end_date):
|
|
|
|
|
# query to return the total notifications sent per day for each channel. NB start and end dates are inclusive
|
|
|
|
|
|
|
|
|
|
daily_volume_stats = db.session.query(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
FactBilling.provider,
|
|
|
|
|
func.sum(FactBilling.notifications_sent).label('sms_totals'),
|
|
|
|
|
func.sum(FactBilling.billable_units).label('sms_fragment_totals'),
|
|
|
|
|
func.sum(FactBilling.billable_units * FactBilling.rate_multiplier).label('sms_chargeable_units'),
|
|
|
|
|
func.sum(FactBilling.billable_units * FactBilling.rate_multiplier * FactBilling.rate).label('sms_cost'),
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.notification_type == SMS_TYPE,
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date,
|
|
|
|
|
).group_by(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
FactBilling.provider,
|
|
|
|
|
).order_by(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
FactBilling.provider,
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
return daily_volume_stats
|
|
|
|
|
|
|
|
|
|
|
2022-03-03 14:47:56 +00:00
|
|
|
def fetch_volumes_by_service(start_date, end_date):
|
|
|
|
|
# query to return the volume totals by service aggregated for the date range given
|
|
|
|
|
# start and end dates are inclusive.
|
|
|
|
|
year_end_date = int(end_date.strftime('%Y'))
|
|
|
|
|
|
|
|
|
|
volume_stats = db.session.query(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
FactBilling.service_id,
|
|
|
|
|
func.sum(case([
|
|
|
|
|
(FactBilling.notification_type == SMS_TYPE, FactBilling.notifications_sent)
|
|
|
|
|
], else_=0)).label('sms_totals'),
|
|
|
|
|
func.sum(case([
|
|
|
|
|
(FactBilling.notification_type == SMS_TYPE, FactBilling.billable_units * FactBilling.rate_multiplier)
|
|
|
|
|
], else_=0)).label('sms_fragments_times_multiplier'),
|
|
|
|
|
func.sum(case([
|
|
|
|
|
(FactBilling.notification_type == EMAIL_TYPE, FactBilling.notifications_sent)
|
|
|
|
|
], else_=0)).label('email_totals'),
|
|
|
|
|
func.sum(case([
|
|
|
|
|
(FactBilling.notification_type == LETTER_TYPE, FactBilling.notifications_sent)
|
|
|
|
|
], else_=0)).label('letter_totals'),
|
|
|
|
|
func.sum(case([
|
|
|
|
|
(FactBilling.notification_type == LETTER_TYPE, FactBilling.notifications_sent * FactBilling.rate)
|
|
|
|
|
], else_=0)).label("letter_cost"),
|
|
|
|
|
func.sum(case(
|
|
|
|
|
[
|
|
|
|
|
(FactBilling.notification_type == LETTER_TYPE, FactBilling.billable_units)
|
|
|
|
|
], else_=0
|
|
|
|
|
)).label('letter_sheet_totals')
|
|
|
|
|
).filter(
|
|
|
|
|
FactBilling.bst_date >= start_date,
|
|
|
|
|
FactBilling.bst_date <= end_date
|
|
|
|
|
).group_by(
|
|
|
|
|
FactBilling.bst_date,
|
|
|
|
|
FactBilling.service_id,
|
|
|
|
|
FactBilling.notification_type
|
|
|
|
|
).subquery()
|
|
|
|
|
|
|
|
|
|
annual_billing = db.session.query(
|
|
|
|
|
func.max(AnnualBilling.financial_year_start).label('financial_year_start'),
|
|
|
|
|
AnnualBilling.service_id,
|
|
|
|
|
AnnualBilling.free_sms_fragment_limit
|
|
|
|
|
).filter(
|
|
|
|
|
AnnualBilling.financial_year_start <= year_end_date
|
|
|
|
|
).group_by(
|
|
|
|
|
AnnualBilling.service_id,
|
|
|
|
|
AnnualBilling.free_sms_fragment_limit
|
|
|
|
|
).subquery()
|
|
|
|
|
|
|
|
|
|
results = db.session.query(
|
|
|
|
|
Service.name.label("service_name"),
|
|
|
|
|
Service.id.label("service_id"),
|
|
|
|
|
Service.organisation_id.label("organisation_id"),
|
|
|
|
|
Organisation.name.label("organisation_name"),
|
2022-03-09 11:55:47 +00:00
|
|
|
annual_billing.c.free_sms_fragment_limit.label("free_allowance"),
|
|
|
|
|
func.coalesce(func.sum(volume_stats.c.sms_totals), 0).label("sms_notifications"),
|
2022-03-03 14:47:56 +00:00
|
|
|
func.coalesce(func.sum(volume_stats.c.sms_fragments_times_multiplier), 0
|
2022-03-09 11:55:47 +00:00
|
|
|
).label("sms_chargeable_units"),
|
|
|
|
|
func.coalesce(func.sum(volume_stats.c.email_totals), 0).label("email_totals"),
|
|
|
|
|
func.coalesce(func.sum(volume_stats.c.letter_totals), 0).label("letter_totals"),
|
|
|
|
|
func.coalesce(func.sum(volume_stats.c.letter_cost), 0).label("letter_cost"),
|
|
|
|
|
func.coalesce(func.sum(volume_stats.c.letter_sheet_totals), 0).label("letter_sheet_totals")
|
2022-03-03 14:47:56 +00:00
|
|
|
).select_from(
|
|
|
|
|
Service
|
|
|
|
|
).outerjoin(
|
|
|
|
|
Organisation, Service.organisation_id == Organisation.id
|
|
|
|
|
).join(
|
|
|
|
|
annual_billing, Service.id == annual_billing.c.service_id
|
|
|
|
|
).outerjoin( # include services without volume
|
|
|
|
|
volume_stats, Service.id == volume_stats.c.service_id
|
|
|
|
|
).filter(
|
|
|
|
|
Service.restricted.is_(False),
|
|
|
|
|
Service.count_as_live.is_(True),
|
|
|
|
|
Service.active.is_(True)
|
|
|
|
|
).group_by(
|
|
|
|
|
Service.id,
|
|
|
|
|
Service.name,
|
|
|
|
|
Service.organisation_id,
|
|
|
|
|
Organisation.name,
|
|
|
|
|
annual_billing.c.free_sms_fragment_limit
|
|
|
|
|
).order_by(
|
|
|
|
|
Organisation.name,
|
|
|
|
|
Service.name,
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
return results
|