@@ -32,7 +32,7 @@ import {
3232 canonicalizeUrl ,
3333 composeBaseURL ,
3434} from '@adobe/spacecat-shared-utils' ;
35- import { Site as SiteModel } from '@adobe/spacecat-shared-data-access' ;
35+ import { Site as SiteModel , Entitlement as EntitlementModel } from '@adobe/spacecat-shared-data-access' ;
3636import { Config } from '@adobe/spacecat-shared-data-access/src/models/site/config.js' ;
3737
3838import RUMAPIClient from '@adobe/spacecat-shared-rum-api-client' ;
@@ -272,6 +272,28 @@ function SitesController(ctx, log, env) {
272272
273273 const accessControlUtil = AccessControlUtil . fromContext ( ctx ) ;
274274
275+ /**
276+ * Ensures the org has an entitlement for the given product and the site is enrolled
277+ * under it. `TierClient.createEntitlement` is idempotent at the library level: existing
278+ * entitlements/enrollments are returned as-is, missing pieces are filled in. Errors
279+ * (invalid tier, DB failures) bubble up to the caller.
280+ *
281+ * Triggered when callers pass an `x-product` header on POST /sites; without the header
282+ * the call is a no-op (preserves backward compatibility for existing integrations).
283+ *
284+ * @param {object } context - Request context (forwarded to TierClient).
285+ * @param {object } site - The newly created or existing site entity.
286+ * @param {string } productCode - Product code from the `x-product` header.
287+ */
288+ const ensureSiteEntitlementAndEnrollment = async ( context , site , productCode ) => {
289+ const tierClient = await TierClient . createForSite ( context , site , productCode ) ;
290+ const {
291+ entitlement,
292+ siteEnrollment,
293+ } = await tierClient . createEntitlement ( EntitlementModel . TIERS . FREE_TRIAL ) ;
294+ log . info ( `Ensured ${ productCode } entitlement ${ entitlement . getId ( ) } and enrollment ${ siteEnrollment ?. getId ( ) } for site ${ site . getId ( ) } ` ) ;
295+ } ;
296+
275297 /**
276298 * Creates a new site or returns an existing one if a site with the same baseURL already exists.
277299 * Implements idempotent-create semantics.
@@ -286,6 +308,13 @@ function SitesController(ctx, log, env) {
286308 *
287309 * Alternative: If strict REST semantics are preferred, 409 Conflict is also valid.
288310 *
311+ * Tier-model compliance: when the caller provides an `x-product` header, the site is
312+ * auto-enrolled into a FREE_TRIAL entitlement for that product (the org-level entitlement
313+ * is created if missing). This keeps the site visible in product-scoped listing endpoints
314+ * (e.g. `GET /organizations/:organizationId/sites`) without a separate manual onboarding
315+ * step. Enrollment failures return 500 — `TierClient.createEntitlement` is idempotent so
316+ * retries are safe (the site lookup will return the already-persisted site).
317+ *
289318 * @param {object } context - Request context containing site data
290319 * @returns {Promise<Response> } HTTP 200 with existing site or 201 with new site
291320 */
@@ -296,27 +325,43 @@ function SitesController(ctx, log, env) {
296325 if ( ! hasText ( context . data ?. baseURL ) ) {
297326 return badRequest ( 'Base URL required' ) ;
298327 }
328+ const productCode = context . pathInfo ?. headers ?. [ 'x-product' ] ;
329+ let site ;
330+ let status ;
299331 try {
300332 const baseURL = composeBaseURL ( context . data . baseURL ) ;
301333 const existingSite = await Site . findByBaseURL ( baseURL ) ;
302334 if ( existingSite ) {
303335 // Idempotent behavior: return existing site with 200 (not 409)
304336 log . info ( `Site already exists for baseURL: ${ baseURL } , returning existing site ${ existingSite . getId ( ) } ` ) ;
305- return createResponse ( SiteDto . toJSON ( existingSite ) , 200 ) ;
337+ site = existingSite ;
338+ status = 200 ;
339+ } else {
340+ site = await Site . create ( {
341+ organizationId : env . DEFAULT_ORGANIZATION_ID ,
342+ ...context . data ,
343+ baseURL, // override with normalized value
344+ } ) ;
345+ updateRumConfig ( site , context ) . catch ( ( e ) => {
346+ log . warn ( `[sites] RUM config update failed for ${ site . getBaseURL ( ) } : ${ e . message } ` ) ;
347+ } ) ;
348+ status = 201 ;
306349 }
307- const site = await Site . create ( {
308- organizationId : env . DEFAULT_ORGANIZATION_ID ,
309- ...context . data ,
310- baseURL, // override with normalized value
311- } ) ;
312- updateRumConfig ( site , context ) . catch ( ( e ) => {
313- log . warn ( `[sites] RUM config update failed for ${ site . getBaseURL ( ) } : ${ e . message } ` ) ;
314- } ) ;
315- return createResponse ( SiteDto . toJSON ( site ) , 201 ) ;
316350 } catch ( error ) {
317351 log . error ( `Error creating site: ${ error . message } ` , error ) ;
318352 return internalServerError ( 'Failed to create site' ) ;
319353 }
354+
355+ if ( hasText ( productCode ) ) {
356+ try {
357+ await ensureSiteEntitlementAndEnrollment ( context , site , productCode ) ;
358+ } catch ( error ) {
359+ log . error ( `Error ensuring ${ productCode } entitlement/enrollment for site ${ site . getId ( ) } : ${ error . message } ` , error ) ;
360+ return internalServerError ( `Failed to ensure ${ productCode } entitlement/enrollment for site` ) ;
361+ }
362+ }
363+
364+ return createResponse ( SiteDto . toJSON ( site ) , status ) ;
320365 } ;
321366
322367 /**
0 commit comments