This project simulates an Ad Agency that manages multiple brands, each with its own daily and monthly budget.
- If a brand exceeds its daily budget, all campaigns are paused for the rest of the day.
- If a brand exceeds its monthly budget, all campaigns are paused for the rest of the month.
- At the start of a new day/month, campaigns are re-evaluated and can be turned back on if budgets allow.
- Some campaigns follow dayparting: they run only during specific hours of the day.
git clone https://github.com/wormer/ad-agency.git
cd ad-agency
pip install -r requirements.txt
src/manage.py migrate
src/manage.py runserver
src/manage.py test aa
id
: integername
: stringmonthly_budget
: decimaldaily_budget
: decimaldayparting
: list of time ranges[["HH:MM", "HH:MM"], ...]
id
: integerbrand_id
: foreign key to Branddatetime
: timestamp of the spendamount
: decimal
- Create new brands
- List all brands
- Update brand name, budgets, and dayparting schedule
- Register new spends for a brand
- Check daily/monthly spending
- Retrieve campaign status (active/inactive)
- Campaign status is determined dynamically when queried.
- Campaigns become active again automatically if budget allows and day/month has reset.
- Dayparting is respected during status checks.
- Spend registration always succeeds regardless of campaign status.
- No DRF or serializers used – the project uses plain Django views and models, without Django REST Framework or input validation.
- No authentication or authorization – all endpoints are open and unauthenticated.
- Campaigns are toggled globally per brand – there is no concept of individual campaigns; the brand as a whole is either active or inactive.
- Campaign status is not cached – it's recalculated every time it is queried based on current time and spends.
- Spends can be registered regardless of campaign status – the system does not prevent spending when a campaign is inactive. It is the client's responsibility to check the status before spending. Once a spend is registered, it is final.
- Server-local time is used – there is no timezone handling or normalization.
- No error handling – the focus is on demonstrating business logic, not on handling edge cases or invalid inputs.
GET /brand/
Returns a list of all brands.
{
"brands": [
{
"id": 1,
"name": "Client"
},
{
"id": 2,
"name": "Company"
}
]
}
POST /brand/
Request:
{
"name": "Company",
"daily_budget": 10,
"monthly_budget": 1000,
"dayparting": [
["10:00", "12:00"],
["15:00", "17:00"]
]
}
Response:
{ "id": 4 }
GET /brand/1/
{
"name": "Company",
"monthly_budget": "1000.00",
"daily_budget": "10.00",
"dayparting": [
["10:00", "12:00"],
["15:00", "17:00"]
]
}
POST /brand/1/
Request:
{
"monthly_budget": 5000,
"dayparting": [
["10:05", "12:05"],
["15:05", "17:05"]
]
}
Response:
{}
GET /brand/1/status/
{
"spends_this_month": "40.70",
"spends_today": "0.00",
"is_active": false
}
POST /spend/1/
Request:
{
"amount": 0.7
}
Response:
{}
function brand_list(request):
if request method is GET:
return a list of all brands with their id and name
if request method is POST:
parse data from the request (name, monthly_budget, daily_budget, dayparting)
create a new brand with the provided data
return the id of the newly created brand
function brand_details(request, brand_id):
find the brand by its id
if request method is POST:
update the brand's name, monthly_budget, daily_budget, or dayparting based on the request data
save the updated brand information
return the brand's details as JSON (name, monthly_budget, daily_budget, dayparting)
function register_spend(request, brand_id):
find the brand by its id
if request method is POST:
parse amount from the request data
create a new Spend record for the brand with the given amount
return a success response
function campaign_status(request, brand_id):
find the brand by its id
get the current time
calculate total spends for today and this month
check if the current time respects the brand's dayparting schedule
determine if the campaign is active based on the budget and dayparting
return the campaign status (spends today, spends this month, and if the campaign is active)