Temă
Database
MySQL 8 or MariaDB 10.6+ (both tested: MySQL 8.0, MariaDB 10.11), utf8mb4 / utf8mb4_unicode_ci, InnoDB, all timestamps stored in UTC. The schema is defined by the Laravel migrations in apps/api/database/migrations; changes go through migrations only. The API reads and writes with Laravel's query builder (no ORM).
Conventions
| Rule | Why |
|---|---|
Tables and columns in snake_case, plural table names | MySQL convention; the GraphQL API speaks camelCase |
id INT UNSIGNED AUTO_INCREMENT, created_at, updated_at on every table | Uniform auditing and ordering |
Money as integers in bani (*_bani, 1 leu = 100 bani) | No floating-point rounding errors |
Fixed sets of values as MySQL ENUM (statuses, roles, reasons) | Validated by the database, 1 byte per value |
Variable-shape data as JSON (specs, rules, steps, addresses, stats) | Avoids dozens of sparse tables; still queryable with JSON_CONTAINS |
Snapshots instead of references where history matters (orders.shipping_address) | An order keeps the address it was shipped to |
Denormalised counters (favorites_count, rating_average, cached_contact_count) | Listing pages never aggregate on read; updated by the services that change them |
| A foreign key is indexed once: when a composite index starts with the column, no separate index | Fewer indexes to maintain on every write |
Append-only tables: audit_logs, consent_events | Legal proof (GDPR art. 7) and accountability |
Domains (51 application tables, plus cache, cache_locks and migrations)
| Module | Tables |
|---|---|
| accounts | users (customers), user_credentials, staff_members (team, role, TOTP secret), auth_sessions, one_time_tokens, totp_step_claims |
| catalog | categories, listings, listing_photos, favorites, saved_searches, banned_words |
| messaging | conversations (one per listing + buyer), messages, offers (with counter-offers) |
| commerce | orders, payments, refunds, payouts, reviews, promotions |
| trust | disputes (one per order), reports (listing, user or message) |
| support | support_tickets, ticket_messages (incl. internal notes), saved_replies |
| notifications | notifications |
| platform | platform_settings (fees, packages, rules, CRM settings as JSON), audit_logs, internal_notes |
| crm | crm_profiles (1:1 with users), consent_events, suppressed_emails, segments, message_templates, campaigns, campaign_deliveries, automations, coupons, coupon_redemptions |
Main relations: a listing belongs to a seller (users) and a category; an order links listing, buyer and seller and owns its payments, refunds, payout, review and at most one dispute; a conversation links listing, buyer and seller.
Commands
From the repository root (they run php artisan in apps/api):
bash
pnpm db:setup # apply pending migrations; load SEED_PROFILE when SEED_DEMO_DATA=true and the DB is empty
pnpm db:status # executed / pending migrations
pnpm db:fresh # drop everything, migrate, load SEED_PROFILE (default demo)
pnpm db:migration add_something # a new, empty migration (php artisan make:migration)
pnpm db:check # the 17 integrity rules; exit code 1 if any is broken
pnpm db:sql # regenerate database/schema.sql, reference-data.sql and upgrades/*.sqlIn Docker the one-shot db-setup container runs php artisan taraba:setup before the API starts. On hosting without a terminal, database/schema.sql and database/reference-data.sql are imported in phpMyAdmin instead (see database/README.md).
Demo data
Two profiles (SEED_PROFILE):
| Profile | What |
|---|---|
reference | Only what the platform needs: team, categories, settings, legal texts, banned words, segments, templates (data file in apps/api/database/seeders/data/) |
demo | Reference plus data generated fresh on every run (app/Modules/Platform/DemoGenerator.php) |
GET /dev/seed empties every table and loads the demo profile again (about 10 seconds), then shows what it created and the accounts. It has no protection, by choice: delete the route from apps/api/routes/api.php before the platform holds real data. pnpm db:fresh does the same from the terminal.
Built for presentations: one simulation of the whole platform over the last 12 months, nothing written in by hand (about 35 seconds). Every number on the dashboards comes from the simulated activity:
- The last two months are the busy season (as after a marketing push): about 35 % of all activity (listings, orders, conversations, messages) falls in them. Sellers post at the upper end of their range then (11–15 a month, companies 21–25; before: 5–11 and 15–21), and part of the sellers and buyers joined then.
- People sign up over the year: 25 people selling (a few early, the others month after month, some in the last two months), 5 verified companies, 50 buyers (part of them in the last two months, some today). Romanian names, cities and phone numbers; valid company CUIs and IBANs. Nothing anyone does comes before their sign-up.
- Listings: every seller posts 5–15 listings in each month of activity (companies 15–25). Each listing goes through moderation (in working hours: minutes by day, until the next morning by night; some rejected or sent back for changes), then sells, expires after 30 days, is taken down or removed, as far as time has come today. Photos, views per day, favourites and paid promotions come with them.
- Orders follow their timeline: paid, shipped, delivered, confirmed, sometimes a dispute (refund, partial refund or in the seller's favour) or, from companies, a return within 14 days. An order's status is the step it reached today, so old orders are finished and recent ones are still on their way. Payments, refunds, payouts (companies after the 14-day right of withdrawal; failed without an IBAN), reviews and the welcome coupon follow from it.
- Around the listings: conversations and offers from interested buyers, notifications, saved searches, abandoned checkouts, reports and support tickets all year (old ones closed, recent ones in the team's queues), identity checks and company requests.
- Marketing: a campaign about every month to the people who had agreed by then (opens, clicks and sales counted from each delivery), a scheduled and a draft campaign, automations (the welcome series enrolled every new account), coupons, consent records.
- Computed at the end: view and favourite counters, seller ratings, coupon uses, last activity, CRM stage and customer value.
Growth on the executive dashboard is the result of people joining over the year, not a curve set by hand.
Accounts for presentations
These exist on every run, with rich activity:
| Account | What it shows |
|---|---|
ioana.dumitru@exemplu.ro | Seller (verified identity): 15 listings in many statuses, sales, purchases, conversations, notifications |
andreea.pop@exemplu.ro | Buyer: many orders in every state, offers, favourites, conversations |
techrecond.srl@firma.ro | Verified company (TechRecond SRL): 20 listings, sales with the 14-day right of withdrawal |
ana.moraru@taraba.ro | Backoffice and CRM, administrator (all screens) |
victor.nastase@taraba.ro, laura.preda@taraba.ro, monica.barbu@taraba.ro, radu.pavel@taraba.ro | Moderator, support, finance, marketing: each sees only their screens |
Password for every account: Taraba2026!. Two-step key of the whole team: KRAXEQSBJ5EUKQ2PKJCVKRCF. The other customers are random and change on every run; /dev/seed lists the team and a few of each kind.
Note: the end-to-end suites in tests/e2e were written for the previous fixed demo data (fixed listings and order codes); with generated data many of them need adapting.
Integrity rules
php artisan taraba:check (app/Console/Commands/CheckIntegrity.php) runs 17 rules, among them: nobody buys their own listing; order seller = listing seller; order totals add up; listings of paid, shipped or finished orders are sold; reviews only for completed orders; paid card orders have a payment; nobody talks to themselves; messages come from the participants; active listings have photos; counters are not below the real rows; prices are positive.