Skip to content

Commit 0a62c5e

Browse files
BroadcastStatics new model and Broadcast update (#591)
* BroadcastStatics new model and Broadcast update * Add trigger to save msg status update in BroadcastStatistics table * Add template_id Broadcast model and update migrations * Add cost, currency and template price in BroadcastStatistic view * Update test * Add service to get template cost * Add test to cost_service
1 parent 3740cdf commit 0a62c5e

File tree

8 files changed

+618
-17
lines changed

8 files changed

+618
-17
lines changed

temba/msgs/cost_service.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import logging
2+
3+
import requests
4+
from weni.internal.clients.base import BaseInternalClient
5+
6+
from django.conf import settings
7+
8+
from temba.templates.models import Template
9+
10+
logger = logging.getLogger(__name__)
11+
12+
CATEGORY_TO_BILLING_FIELD = {
13+
"MARKETING": "marketing",
14+
"UTILITY": "utility",
15+
"AUTHENTICATION": "authentication",
16+
"SERVICE": "service",
17+
}
18+
19+
20+
class BillingInternalClient(BaseInternalClient):
21+
def __init__(self, base_url=None, authenticator=None):
22+
base_url = base_url or getattr(settings, "BILLING_BASE_URL", None)
23+
super().__init__(base_url=base_url, authenticator=authenticator)
24+
25+
def get_pricing(self, project=None):
26+
params = {}
27+
if project:
28+
params["project"] = project
29+
response = requests.get(
30+
self.get_url("/api/v1/meta-pricing/"), headers=self.authenticator.headers, params=params, timeout=10
31+
)
32+
response.raise_for_status()
33+
return response.json()
34+
35+
36+
def get_template_price_and_currency_from_api(template_id=None):
37+
"""
38+
Search for the template price and currency in the external pricing API.
39+
Returns (template_price, currency). If it fails, returns (0, 'BRL').
40+
If template_id is provided, uses its category to select the correct price field.
41+
"""
42+
try:
43+
client = BillingInternalClient()
44+
template_price = 0
45+
if template_id:
46+
try:
47+
template = Template.objects.get(pk=template_id)
48+
project_uuid = template.org.proj_uuid
49+
data = client.get_pricing(project=project_uuid)
50+
currency = data.get("currency", "BRL")
51+
category = template.category
52+
if category not in CATEGORY_TO_BILLING_FIELD:
53+
logger.warning(f"Category {category} not mapped to billing. Using 'marketing' as fallback.")
54+
billing_field = CATEGORY_TO_BILLING_FIELD.get(category, "marketing")
55+
template_price = float(data.get("rates", {}).get(billing_field, 0))
56+
except Template.DoesNotExist:
57+
template_price = float(data.get("marketing", 0))
58+
else:
59+
template_price = float(data.get("marketing", 0))
60+
return template_price, currency
61+
except Exception:
62+
return 0, "BRL"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 3.2.22 on 2025-08-11 17:48
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("msgs", "0166_auto_20241112_1456"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="broadcast",
15+
name="is_bulk_send",
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AddField(
19+
model_name="broadcast",
20+
name="name",
21+
field=models.CharField(blank=True, max_length=255, null=True),
22+
),
23+
migrations.AddField(
24+
model_name="broadcast",
25+
name="template_id",
26+
field=models.IntegerField(blank=True, null=True),
27+
),
28+
]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 3.2.22 on 2025-08-20 14:11
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("orgs", "0092_org_proj_uuid"),
12+
("msgs", "0167_auto_20250811_1748"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="BroadcastStatistics",
18+
fields=[
19+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
20+
("processed", models.IntegerField(default=0)),
21+
("sent", models.IntegerField(default=0)),
22+
("delivered", models.IntegerField(default=0)),
23+
("failed", models.IntegerField(default=0)),
24+
("read", models.IntegerField(default=0)),
25+
("contact_count", models.IntegerField(default=0)),
26+
("cost", models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
27+
("template_price", models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
28+
("currency", models.CharField(blank=True, max_length=20, null=True)),
29+
("created_on", models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
30+
("modified_on", models.DateTimeField(default=django.utils.timezone.now)),
31+
(
32+
"broadcast",
33+
models.ForeignKey(
34+
on_delete=django.db.models.deletion.PROTECT, related_name="statistics", to="msgs.broadcast"
35+
),
36+
),
37+
(
38+
"org",
39+
models.ForeignKey(
40+
on_delete=django.db.models.deletion.PROTECT, related_name="broadcast_statistics", to="orgs.org"
41+
),
42+
),
43+
],
44+
),
45+
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from django.db import migrations
2+
3+
TRIGGER_FUNCTION = """
4+
CREATE OR REPLACE FUNCTION update_broadcast_statistics_on_msg_status()
5+
RETURNS TRIGGER AS $$
6+
BEGIN
7+
-- Only process if the broadcast_id or status changed
8+
IF NEW.broadcast_id IS NULL OR NEW.status IS NOT DISTINCT FROM OLD.status THEN
9+
RETURN NEW;
10+
END IF;
11+
12+
-- Only process for bulk sends
13+
IF NOT EXISTS (
14+
SELECT 1 FROM msgs_broadcast b WHERE b.id = NEW.broadcast_id AND b.is_bulk_send
15+
) THEN
16+
RETURN NEW;
17+
END IF;
18+
19+
IF NEW.status = 'S' THEN
20+
UPDATE msgs_broadcaststatistics
21+
SET sent = sent + 1,
22+
cost = COALESCE(cost, 0) + COALESCE(template_price, 0)
23+
WHERE broadcast_id = NEW.broadcast_id;
24+
25+
ELSIF NEW.status = 'D' THEN
26+
UPDATE msgs_broadcaststatistics
27+
SET delivered = delivered + 1
28+
WHERE broadcast_id = NEW.broadcast_id;
29+
30+
ELSIF NEW.status = 'F' THEN
31+
UPDATE msgs_broadcaststatistics
32+
SET failed = failed + 1
33+
WHERE broadcast_id = NEW.broadcast_id;
34+
35+
-- If it failed directly from the queue, consider it as 'processed'
36+
IF OLD.status = 'Q' THEN
37+
UPDATE msgs_broadcaststatistics
38+
SET processed = processed + 1,
39+
modified_on = NOW()
40+
WHERE broadcast_id = NEW.broadcast_id;
41+
END IF;
42+
43+
ELSIF NEW.status = 'W' THEN
44+
UPDATE msgs_broadcaststatistics
45+
SET processed = processed + 1,
46+
modified_on = NOW()
47+
WHERE broadcast_id = NEW.broadcast_id;
48+
49+
ELSIF NEW.status = 'V' THEN
50+
UPDATE msgs_broadcaststatistics
51+
SET read = read + 1
52+
WHERE broadcast_id = NEW.broadcast_id;
53+
END IF;
54+
55+
RETURN NEW;
56+
END;
57+
$$ LANGUAGE plpgsql;
58+
"""
59+
60+
TRIGGER = """
61+
CREATE TRIGGER trg_update_broadcast_statistics_on_msg_status
62+
AFTER UPDATE OF status ON msgs_msg
63+
FOR EACH ROW
64+
EXECUTE FUNCTION update_broadcast_statistics_on_msg_status();
65+
"""
66+
67+
DROP_TRIGGER = """
68+
DROP TRIGGER IF EXISTS trg_update_broadcast_statistics_on_msg_status ON msgs_msg;
69+
DROP FUNCTION IF EXISTS update_broadcast_statistics_on_msg_status();
70+
"""
71+
72+
73+
class Migration(migrations.Migration):
74+
75+
dependencies = [
76+
("msgs", "0168_broadcaststatistics"),
77+
]
78+
79+
operations = [
80+
migrations.RunSQL(TRIGGER_FUNCTION, DROP_TRIGGER),
81+
migrations.RunSQL(TRIGGER, DROP_TRIGGER),
82+
]

0 commit comments

Comments
 (0)