# Google Ads Campaign Automation — Backend

Node.js/Express backend that automates Google Ads App Campaign creation from
a Google Play Store URL: import app metadata, configure targeting/budget/
bidding, preview the exact campaign/ad-group structure, then create it via
the Google Ads API.

## Architecture

Layered / repository pattern:

```
src/
  config/         env, logger (winston), db pool, static reference data
  db/migrations/  SQL migrations + runner (src/db/migrate.js)
  repositories/   raw SQL data access, one per table group
  services/       business logic (PlayStoreService, CampaignPlanningService,
                  CampaignService, GoogleAdsService, ReferenceDataService)
  controllers/    thin HTTP handlers, no business logic
  routes/         Express routers
  validators/     Zod request schemas
  middlewares/    auth (JWT), rate limiting, error handling, request logging
  dtos/           response shaping (snake_case DB rows -> camelCase API)
  utils/          ApiError, name generation, micros conversion, Play Store URL parsing
```

## Setup

```bash
npm install
cp .env.example .env   # fill in DATABASE_URL and Google Ads credentials
npm run migrate        # applies src/db/migrations/*.sql
npm run dev
```

`GET /health` confirms the server is up without needing a database connection.

## Google Ads credentials

Campaign creation (`POST /campaigns/:id/create`) requires
`GOOGLE_ADS_CLIENT_ID`, `GOOGLE_ADS_CLIENT_SECRET`, `GOOGLE_ADS_DEVELOPER_TOKEN`,
`GOOGLE_ADS_REFRESH_TOKEN`, and `GOOGLE_ADS_LOGIN_CUSTOMER_ID`. Until these are
set, every other endpoint (import, preview, draft creation, reference data)
works fully against Postgres — only the live Google Ads mutation is gated.

Requires `google-ads-api@^24.1.0` or later — v20 (and any version whose
underlying Google Ads API version has been deprecated) gets hard-rejected
by Google's servers with "Version vNN is deprecated." Check
`npm view google-ads-api versions` if calls start failing outright.

## Authentication

All `/api/applications/*` and `/api/campaigns/*` routes require a JWT
(`Authorization: Bearer <token>`), obtained via `POST /api/auth/login`.
Seed the first admin user with `npm run seed` (reads `ADMIN_EMAIL` /
`ADMIN_NAME` / `ADMIN_PASSWORD` from `.env`; safe to re-run to rotate the
password). `/api/countries`, `/api/languages`, `/api/bidding-strategies`
stay public (static reference data).

To generate `GOOGLE_ADS_REFRESH_TOKEN`, visit `/api/auth/google` in a
browser (see `docs/GOOGLE_ADS_REFRESH_TOKEN_SETUP.md`) — that's a separate,
one-time bootstrap flow, not part of the app's own login.

## App Campaign bidding

App Campaigns use a different bidding model than Search/Display — a
"focus" (`app_campaign_setting.bidding_strategy_goal_type`) plus an
optional numeric target, not the generic Maximize Conversions / Target CPA
/ Manual CPC list. See `config/referenceData/biddingStrategies.js` for the
three supported goals and `GoogleAdsService.appCampaignBiddingFields` for
the mapping to real API enums. In-app actions for the "In-app action
volume/value" goals are fetched live from the connected Google Ads account
via `GET /api/campaigns/conversion-actions?applicationId=` — they're
account-specific, not static reference data.

## Reference data

`config/referenceData/{countries,languages}.js` are curated seed lists
(major markets), not the full Google Ads geo/language catalog. Flagged
in-file as seed data to verify against the live `GeoTargetConstantService`
/ `LanguageConstantService` before production use.

## API surface

```
POST /api/auth/login                          { email, password } -> JWT
GET  /api/auth/me                             requires Bearer token
GET  /api/auth/google                         one-time refresh-token bootstrap
GET  /api/auth/google/callback

POST /api/applications/import                 { playStoreUrl }        [auth]
GET  /api/applications/:id                                            [auth]
GET  /api/applications                                                [auth]

GET  /api/countries
GET  /api/languages
GET  /api/bidding-strategies

POST /api/campaigns/preview                   plan without saving     [auth]
POST /api/campaigns                           persist as DRAFT        [auth]
GET  /api/campaigns                                                   [auth]
GET  /api/campaigns/conversion-actions?applicationId=                 [auth]
GET  /api/campaigns/:id                                               [auth]
GET  /api/campaigns/:id/logs                                          [auth]
POST /api/campaigns/:id/create                create in Google Ads    [auth]
```

## Campaign creation payload shape

```json
{
  "applicationId": "uuid",
  "countries": [
    {
      "countryCode": "US",
      "languages": [
        { "languageCode": "en", "adGroupCount": 3 },
        { "languageCode": "pt", "adGroupCount": 2 }
      ]
    }
  ],
  "dailyBudget": 50,
  "currencyCode": "USD",
  "biddingStrategy": "IN_APP_ACTION_VALUE",
  "biddingConfig": {
    "conversionActionResourceNames": ["customers/123/conversionActions/456"],
    "targetRoas": 0.94
  },
  "euPoliticalAds": "NO",
  "networkSearch": true,
  "networkDisplay": true,
  "networkSearchPartners": true,
  "startDate": "2026-08-04"
}
```

One campaign is created per country; ad groups are generated per language
using `<App Name> - <Country> - <Language> - AG0N`.

## Not yet implemented

- CSRF token issuance
- Live sync of full Google Ads geo/language catalogs (currently a curated seed list)

NODE_ENV=production npm run migrate