Branch Audit · Yugam Django
The full Yugam 2026 build cycle. 761 commits that never reached master — a new WhatsApp bot API, the PayU→Razorpay migration, coupons, a dashboard cache layer, and the POS rewrite.
This is not a feature branch. It is the release line for Yugam 2026 (5–7 March 2026), and it absorbed eleven other branches through 155 merge commits — dev-UserDashboard (26), dev-Dashboard-Optimization (17), dev-WhatsappWebhook, dev-WhatsappAPI, dev-Angadi, dev-Coupon, dev-BottomBar, dev-Updates. Commit messages like “fixes night before yugam2026” confirm it ran the live fest.
master is 761 commits behind and 0 ahead: the release line in the docs has received none of this. dev-React is branched directly off this tip, so the new frontend inherits everything audited below.
The single largest addition: an entire new Django app serving the WhatsApp bot at /api/v1/whatsapp/. Views are split across six modules — core, workshops, proshows, payments & teams, auth & combos, webhooks. whatsapp_api/views.py alone is 4,906 lines.
WhatsAppLoginToken (passwordless login links) and FormFillToken (shareable, expiring dynamic-form links for unauthenticated submission).WhatsAppAPIAuthMiddleware gates the whole prefix on an X-API-Key header, excluding the Razorpay webhook and the public success page.verify_whatsapp_api.py — a URL-name resolution check, the only automated verification in the repo.PAYU_INFO is commented out and replaced by RAZORPAY_INFO. Payment links, order fetch, and an HMAC-SHA256 webhook with hmac.compare_digest — the signature verification itself is correctly written.
Also new here: offline bulk import (BulkImportBatch / BulkImportEntry + a Celery task that ingests a CSV of participants and marks them paid), online refund yugam_id, and a blacklist check woven through nine payment paths.
A single validate_coupon() helper — a genuine improvement over this codebase's usual habit of re-deriving pricing per app. Coupons scope to exactly one of event / workshop / seat / general pass, support percent or flat discount, gender restriction, expiry, and a redemption cap.
Wired into web checkout, the WhatsApp API (/coupons/validate/), and proshow booking. Admin gets coupon list/add/edit plus a coupon analytics page.
Bulk-quantity discount tiers per seat section: min/max quantity, percent off, deadline, redemption cap, and separate tiers for early-bird vs. normal pricing. Yugam360Booking gains amount_paid (for revenue stats) and bulk_tier_used.
Note that discount_type/discount_value were created in 0038 and removed again in 0039 in favour of a plain discount_percent — the tier model was reworked mid-flight.
The stall system was rebuilt: Invoice, LineItems, Vendor, RefundDetails models; a real POS screen with a finish-bill and cancel-invoice API; vendor create/edit; a product approval queue; and products/tasks.py.
Two parallel analytics suites — vendor-facing (sales summary, top revenue, top quantity, sales by date, product performance) and admin-facing (platform summary, vendor comparison, per-vendor detail). The app is now routed in yugam/urls.py; on master it was commented out.
A new developer-admin analytics section: overview, events, workshops, financials, check-in, proshow, conclaves, accommodation. Each gated behind check_developer_admin with shared date/int filter parsing.
Certificate generation moved from the pdftk approach to a reportlab overlay engine: sentence templates per certificate type (event participation, event achievement, workshop participation), Poppins for Latin and Nirmala UI for Tamil, content boxes measured in points against A4.
New 2026 KCT and KCLAS templates for all three types, plus async upload of finished certificates to S3 and a delete-after-5-minutes cleanup task.
Outbound WhatsApp is now routed through an external webhook dashboard (USE_WEBHOOK_DASHBOARD = True, pointing at webhook.iqubekct.ac.in), with the direct Graph API kept as a fallback branch. WhatsappMessageLog gains request_id, template_name, created_at, sent_at and a derived status property; a callback URL reports delivery back.
sendmail/image_utils.py downsizes any event/workshop/proshow image to a <100 KB JPG for WhatsApp template headers, stored in a new whatsapp_image field on Event, Workshop and Yugam360Image and populated by post-save signals.
Proshow tickets now render as a PDF and are delivered over WhatsApp.
In production, default file storage is now PublicMediaStorage on S3 under an uploads/ prefix; dev stays on the filesystem. Profile QR codes are deliberately pinned to local media (QR_CODE_LOCATION), which works because web1/2/3 and the workers all mount the same yugam26 volume.
yugam25→yugam26; Postgres moved to host port 5440, Redis to 6380./static/ and /media/ directly with gzip and long cache headers, instead of proxying everything to Django.server_name widened to *.kctyugam.com, and SubdomainConclaveMiddleware is now actually enabled in MIDDLEWARE — per-conclave subdomains are live.--reload removed from production (correct) and access/error logging turned on; Celery gains --max-tasks-per-child=50.ConclaveImage model, gallery admin, dedicated 2026 conclave detail page.event-detail26, workshop-detail26, conclave-detail26, plus four WhatsApp form-fill templates.highlights field; workshop speaker list/edit CRUD; ComboDynamic.excluded_events; Sponsor.is_internal; ambassador is_bot.decline_team_invitation; a user-facing my_transactions page; a vendor admin role.add_search_indexes.sql — 11 hand-written Postgres indexes for event/workshop title search and active-flag filters.This was the dev-Dashboard-Optimization work (17 merges) and it is the most consequential non-feature change on the branch. The public dashboard — every event, workshop, combo, category, domain, proshow, newsfeed and day schedule — was previously assembled per request. It is now built once into a single consolidated payload and served from Redis, warmed by Celery, invalidated by model signals, and paginated through a per-tab AJAX cache.
| Key | Written by | TTL | Invalidated by |
|---|---|---|---|
| dashboard:public:consolidated:v4 | _build_all_public_data(), internally |
600 s | nothing |
| dashboard:public:consolidated:v1 | dashboard_cache() via get_or_set; Celery warm_dashboard_cache |
600 s | nothing |
| dashboard:ajax:{tab}:page:{n} | AJAX pagination view; warm_ajax_cache_simple |
300 s | delete_pattern on save |
| dashboard:cache:version | clear_dashboard_cache() — unix timestamp |
none | rewritten on save |
| public_data_* (categories, domains, proshows) | cache.get_or_set inside the builder |
varies | nothing |
| blacklist_user_ids | inlined in 9 places in payment/views.py |
300 s | nothing |
Supporting pieces: events/signals.py hooks post_save/post_delete on Event, Workshop, Category, SubCategory and Domain; USE_CACHED_ENDPOINT in settings toggles the cached path per template; dashboard-cache.js and dashboard-controller.js drive the frontend against the version key; a cache_status debug endpoint reports whether the payload is live; and nginx adds gzip plus static/media caching in front of all of it.
Where it doesn't hold together
The three moving parts were built against three different key names. The builder caches under …:v4, its caller caches the same payload again under …:v1, and the signal handler deletes only dashboard:ajax:* and dashboard:cache:*. So the payload is stored twice, and an admin editing an event invalidates neither copy — the change appears when the 600-second TTL lapses, not when the signal fires. The signals, the logging around them, and the version key all imply instant invalidation that does not actually happen. See findings 6 and 7.
GenerateWhatsAppLoginTokenView calls WhatsAppLoginToken.generate_token(user, phone), but generate_token() is a zero-argument @staticmethod that just returns a random string. The constructor is create_token(user, phone). Every request raises TypeError, which the broad except Exception converts into a 500 internal_error. Passwordless login from the bot has never worked on this branch.
whatsapp_api/views_auth_combos.py:36 · model at whatsapp_api/models.py:36, 55
A live key_id and key_secret are hardcoded in tracked source. Separately, webhook_secret is set to the same string as key_secret with the comment “To be configured in Razorpay Dashboard”. Either the dashboard webhook secret was never set — in which case every webhook fails signature verification and payment confirmations arrive only via the polling task — or the API secret was reused as the webhook secret, which doubles the blast radius of a single leak. Worth checking which, because it changes whether webhooks were live during the fest.
yugam/settings.py · RAZORPAY_INFO · consumed at whatsapp_api/webhooks.py:631
location / — the proxy pass to Django, i.e. every HTML page including logged-in dashboards, transaction lists and admin screens — carries add_header Cache-Control "public, max-age=300". Any shared cache or browser back-button is entitled to store and re-serve one user's authenticated page for five minutes. The static and media blocks above it are fine; it is the catch-all that is wrong.
nginx/nginx.conf · location /
In _process_payment_success, the proshow bulk-tier block calls SeatBulkPrice.objects.filter(...).select_for_update().first() with no enclosing atomic() and no ATOMIC_REQUESTS — Django raises TransactionManagementError. It is caught by except Exception: capture_exception(e), so nothing surfaces except Sentry noise. Two consequences: the tier's used_count never increments, so bulk-tier caps are never enforced; and amount_paid is only written in the else branch (no order id), so proshow revenue figures on the Razorpay path are undercounted.
payment/views.py:6441 · nearest atomic block is at 6790
WHATSAPP_BOT_API_KEY reads from the environment but falls back to a hardcoded constant — the same value as the existing committed API_KEY — so a missing env var silently downgrades to a key that is in the repository. The middleware compares with != rather than hmac.compare_digest. Behind that one key sit endpoints that create registrations, generate payment links, reserve seats, and mint login links for arbitrary phone numbers.
yugam/settings.py · whatsapp_api/middleware.py:44
clear_dashboard_cache() deletes dashboard:ajax:* and dashboard:cache:*. The consolidated payload lives under dashboard:public:consolidated:v1 and :v4 — neither matches either pattern. An event edited in admin therefore takes up to 600 seconds to appear, and the same payload is held twice in Redis because the builder caches internally under v4 and the caller re-caches the return value under v1.
events/signals.py · userdashboard/views.py:6146, 6499, 6553
warm_dashboard_cache is registered in setup_periodic_tasks at 30-second intervals and in CELERY_BEAT_SCHEDULE at */9 minutes. Because all three workers run with -B, both registrations fire on each of worker1/2/3. The comment says “before 1 min TTL expires” but the actual TTL is 600 s, so the full public payload — every event, workshop and proshow — is rebuilt roughly six times per 30 seconds against a cache entry that is valid for ten minutes.
yugam/celery.py:43–46 · yugam/settings.py CELERY_BEAT_SCHEDULE
get_proshow_booking_base_amount() is called from six places — web checkout, cashier, payment, userdashboard, webcontent, yugamtheme — but not from whatsapp_api/views_proshows.py, which computes seat.price × ticket_count directly. A bot user booking eight tickets pays list price where a web user gets the tier discount. This adds a seventh independent pricing site to the duplication map.
whatsapp_api/views_proshows.py:178–187
validate_coupon() checks used_count >= max_count; the increment happens much later, at payment success, via F("used_count") + 1. The increment is atomic but the check is not, so concurrent redemptions overshoot the cap. The same holds for SeatBulkPrice, where the filter used_count < max_count ignores how many seats the current booking is asking for — a cap of 100 with 99 used still admits a 10-seat booking.
payment/coupon_utils.py:51 · payment/views.py:6080 · webcontent/proshow_pricing.py:50
SESSION_COOKIE_DOMAIN and CSRF_COOKIE_DOMAIN are hardcoded to .yugam.in. On any other host — kctyugam.com, which this branch still references in whatsapp_service.py and the nginx site config, or localhost during development — the browser rejects the cookie and login fails with no error. This needs to be environment-derived before anyone runs the branch outside production.
yugam/settings.py · near SESSION_COOKIE_AGE
products.urls and yugamtheme.urls are each included twice, plus a third products route without a trailing slash — the same comment block appears twice in a row. Django resolves on first match so behaviour is unaffected, but every reverse() and every future edit now has two candidate sites, and yugamtheme being a catch-all makes the ordering load-bearing.
yugam/urls.py:176–184
Adding auth to the log viewer is the right instinct, but the user file with its password hash is tracked in git and bind-mounted into the container. It sits alongside a dozzle-users_template.txt that presumably exists because someone knew the real file shouldn't be there. The repo's own rule is that no new secret gets committed.
dozzle-users.yml · docker-compose-prod.yml dozzle service
Every Event and Workshop save triggers a post-save signal that opens the image (possibly over S3), re-encodes it to a <100 KB JPG, saves it back with a second save(), then walks up to 200 cache keys — all inside the admin request. It also emits multi-line = banner logs per save. This belongs in a Celery task. Related: COUPON_FLOW print() tracing is still live in payment/views.py.
events/signals.py · payment/views.py:6051, 6081
add_search_indexes.sql creates eleven indexes that the dashboard's title search and active-flag filters depend on, but it is a loose file someone has to remember to run. A fresh database — or the next environment — gets the queries without the indexes. These should be AddIndex operations, or at minimum a RunSQL migration.
add_search_indexes.sql
YUGAM_YEAR is hardcoded back to 26 (master derived it from the current year). YUGAM_GENERAL_EVENT_DATES has entries 1 and 2 commented out, leaving only key 3 — a fest-week edit that was never reverted, and date-keyed logic elsewhere assumes three days. CONVENIENCE_FEE is now 0 in both branches of the conditional. DAY_1/2/3 and YUGAM_DATES are fixed to March 2026.
yugam/settings.py
Unchanged from the documented baseline, but the exposure is now much larger: a new payment gateway, a coupon engine, a discount tier system, a POS with refunds, and 87 unauthenticated-by-session API endpoints all shipped with no automated verification beyond verify_whatsapp_api.py, which only asserts that URL names resolve.
repo-wide
Because master is strictly behind, a merge is mechanically a fast-forward — there are no conflicts to resolve. The risk is entirely operational.
CheckConstraint on FormFillToken and a field added then removed on SeatBulkPrice. On a database with production data, 0039's discount_percent arrives with preserve_default=False and a default of 10 — any rows created between 0038 and 0039 silently take a 10% tier.yugam25 stack starts against empty volumes unless the data is migrated first.FRONTEND_URL = https://yugam.in mean the branch does not run cleanly anywhere else without edits.Rotate the Razorpay credentials, move them to os.environ, and set a distinct webhook secret in the Razorpay dashboard. Then confirm from the logs whether webhooks were actually verifying during the fest — if they weren't, payment confirmation was running on the 5-minute polling task alone.
Drop the Cache-Control: public header from the nginx location / block. Keep it on /static/ and /media/.
Untrack dozzle-users.yml, regenerate the hash, and gitignore it next to the other env files.
Fix finding 1 (create_token, one line) and finding 4 (wrap the bulk-tier block in atomic(), and set amount_paid unconditionally). Both are small and both are currently invisible because the failures are swallowed.
Settle the cache on one key. Have _build_all_public_data() build without caching, cache once at the caller, and add that key to clear_dashboard_cache(). Then delete one of the two beat registrations and set the interval against the real TTL.
Route whatsapp_api/views_proshows.py through get_proshow_booking_base_amount() so bulk tiers apply on every path, and derive the cookie domain from the environment.
Before the technical/cultural split work starts on dev-React, decide whether whatsapp_api is the template for the new API surface. It is the cleanest thing on this branch — DRF views, serializers, real locking in the seat reservation path — and also the largest single body of untested code in the repository. Whichever way that goes, add locking around every counter (coupons, tiers, seats, early-bird caps) before the next fest, and move add_search_indexes.sql into the migration graph.