late-sdk 0.0.897 → 0.0.899

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/docs/AdCampaignsApi.md +11 -7
  4. data/docs/AdCreative.md +4 -0
  5. data/docs/AdCreativesApi.md +2 -2
  6. data/docs/BoostPostRequest.md +2 -0
  7. data/docs/CreateAdCreative201Response.md +5 -1
  8. data/docs/CreateAdCreativeRequest.md +4 -2
  9. data/docs/CreateCallAdRequest.md +2 -0
  10. data/docs/CreateMessagingAdRequest.md +2 -0
  11. data/docs/CreateStandaloneAdRequest.md +5 -3
  12. data/docs/CreateStandaloneAdRequestCreativesInner.md +4 -0
  13. data/docs/CreateStandaloneAdRequestPromotedObject.md +2 -2
  14. data/docs/CtwaAdRequestBody.md +2 -0
  15. data/docs/CtwaAdRequestBodyCreativesInner.md +2 -0
  16. data/docs/MetaPromotion.md +26 -0
  17. data/docs/MetaPromotionStatus.md +15 -0
  18. data/docs/UpdateAdRequestCreative.md +4 -0
  19. data/lib/zernio-sdk/api/ad_campaigns_api.rb +9 -6
  20. data/lib/zernio-sdk/api/ad_creatives_api.rb +4 -4
  21. data/lib/zernio-sdk/models/ad_creative.rb +42 -1
  22. data/lib/zernio-sdk/models/boost_post_request.rb +13 -1
  23. data/lib/zernio-sdk/models/create_ad_creative201_response.rb +44 -4
  24. data/lib/zernio-sdk/models/create_ad_creative_request.rb +11 -2
  25. data/lib/zernio-sdk/models/create_call_ad_request.rb +13 -1
  26. data/lib/zernio-sdk/models/create_messaging_ad_request.rb +13 -1
  27. data/lib/zernio-sdk/models/create_standalone_ad_request.rb +12 -3
  28. data/lib/zernio-sdk/models/create_standalone_ad_request_creatives_inner.rb +23 -1
  29. data/lib/zernio-sdk/models/create_standalone_ad_request_dynamic_creative.rb +1 -1
  30. data/lib/zernio-sdk/models/create_standalone_ad_request_promoted_object.rb +2 -2
  31. data/lib/zernio-sdk/models/ctwa_ad_request_body.rb +13 -1
  32. data/lib/zernio-sdk/models/ctwa_ad_request_body_creatives_inner.rb +35 -1
  33. data/lib/zernio-sdk/models/meta_promotion.rb +275 -0
  34. data/lib/zernio-sdk/models/meta_promotion_status.rb +41 -0
  35. data/lib/zernio-sdk/models/update_ad_request_creative.rb +45 -2
  36. data/lib/zernio-sdk/version.rb +1 -1
  37. data/lib/zernio-sdk.rb +2 -0
  38. data/openapi.yaml +132 -13
  39. data/spec/api/ad_campaigns_api_spec.rb +4 -3
  40. data/spec/api/ad_creatives_api_spec.rb +2 -2
  41. data/spec/models/ad_creative_spec.rb +12 -0
  42. data/spec/models/boost_post_request_spec.rb +10 -0
  43. data/spec/models/create_ad_creative201_response_spec.rb +12 -0
  44. data/spec/models/create_ad_creative_request_spec.rb +6 -0
  45. data/spec/models/create_call_ad_request_spec.rb +10 -0
  46. data/spec/models/create_messaging_ad_request_spec.rb +10 -0
  47. data/spec/models/create_standalone_ad_request_creatives_inner_spec.rb +16 -0
  48. data/spec/models/create_standalone_ad_request_spec.rb +6 -0
  49. data/spec/models/ctwa_ad_request_body_creatives_inner_spec.rb +10 -0
  50. data/spec/models/ctwa_ad_request_body_spec.rb +10 -0
  51. data/spec/models/meta_promotion_spec.rb +64 -0
  52. data/spec/models/meta_promotion_status_spec.rb +30 -0
  53. data/spec/models/update_ad_request_creative_spec.rb +16 -0
  54. data/zernio-sdk-0.0.899.gem +0 -0
  55. metadata +10 -2
  56. data/zernio-sdk-0.0.897.gem +0 -0
data/openapi.yaml CHANGED
@@ -886,6 +886,9 @@ components:
886
886
  The route enforces this at the Zod boundary; OpenAPI's
887
887
  `required` cannot express the OR cleanly.
888
888
  properties:
889
+ creativeFeatures:
890
+ $ref: '#/components/schemas/MetaCreativeFeatures'
891
+ description: 'Meta enhancement settings for single or attached ads, and defaults for creatives[]. An item replaces the entire map, including with an empty object.'
889
892
  accountId:
890
893
  type: string
891
894
  minLength: 1
@@ -986,6 +989,9 @@ components:
986
989
  type: string
987
990
  pattern: '^\d+_\d+$'
988
991
  description: 'Messaging and CTWA only. Raw Facebook pageId_postId reference, used as object_story_id even with an Instagram account. Mutually exclusive with existingPostId and fresh creative fields.'
992
+ creativeFeatures:
993
+ $ref: '#/components/schemas/MetaCreativeFeatures'
994
+ description: 'Replaces the top-level creativeFeatures map for this item. Omit to inherit; an empty object clears inherited enrollment choices.'
989
995
  headline:
990
996
  type: string
991
997
  minLength: 1
@@ -8999,6 +9005,36 @@ components:
8999
9005
  jobFunctions: { type: array, items: { type: string }, description: "LinkedIn B2B only." }
9000
9006
  audienceInclude: { type: array, items: { type: string }, description: 'Platform audience IDs to include, as returned by GET /v1/ads/audiences (Meta custom audience ids, TikTok audience ids, Pinterest customer list ids, LinkedIn segment ids (the platformAudienceId from GET /v1/ads/audiences; Zernio resolves it to the targetable LinkedIn ad segment, an unknown id returns 400), Google user list ids, X custom audience ids). Not supported on OpenAI (400).' }
9001
9007
  audienceExclude: { type: array, items: { type: string }, description: 'Platform audience IDs to exclude; same ID formats as audienceInclude. Not supported on OpenAI (400).' }
9008
+ MetaCreativeFeatures:
9009
+ type: object
9010
+ additionalProperties: { type: string, enum: [OPT_IN, OPT_OUT] }
9011
+ propertyNames: { pattern: '^[a-z0-9_]+$' }
9012
+ description: 'Meta Advantage+ creative enhancements. Map snake_case feature names to OPT_IN or OPT_OUT; Meta validates supported keys and unspecified features default to OPT_OUT. auto_promotion_tag is an enhancement; use the separate promotion field for an explicit offer. The deprecated standard_enhancements bundle is rejected by Meta.'
9013
+ example: { auto_promotion_tag: OPT_IN }
9014
+ MetaPromotion:
9015
+ type: [object, "null"]
9016
+ description: 'Meta explicit Promotion offer. Maps to creative_sourcing_spec.promotion_metadata_spec with promotion_source ADVERTISER_INPUT. Dates become Unix seconds. Send null to omit an explicit offer on a new creative or remove it when rebuilding. Creation success alone does not confirm application: inspect promotionStatus in the response.'
9017
+ required: [type, value]
9018
+ properties:
9019
+ type:
9020
+ type: string
9021
+ enum: [AMOUNT_OFF, FREE_RETURN, FREE_SHIPPING, PERCENTAGE_OFF, PROMO_CODE]
9022
+ description: 'Promotion type accepted by Meta. PERCENTAGE_OFF values cannot exceed 100.'
9023
+ value: { type: number, minimum: 0, description: 'Nonnegative promotion value passed to Meta unchanged. AMOUNT_OFF units are not confirmed, including major versus minor currency units. For PERCENTAGE_OFF this is the percentage discount, at most 100.' }
9024
+ code: { type: string, minLength: 1, description: 'Optional promotion code.' }
9025
+ startDate: { type: string, format: date-time, description: 'Optional ISO 8601 start timestamp with a timezone offset or Z.' }
9026
+ endDate: { type: string, format: date-time, description: 'Optional ISO 8601 end timestamp with a timezone offset or Z. Must be after startDate when both are set.' }
9027
+ example:
9028
+ type: PERCENTAGE_OFF
9029
+ value: 20
9030
+ code: SAVE20
9031
+ startDate: '2026-10-01T00:00:00Z'
9032
+ endDate: '2026-10-31T23:59:59Z'
9033
+ MetaPromotionStatus:
9034
+ type: string
9035
+ enum: [applied, not_returned, unavailable]
9036
+ description: 'Meta creative readback result. applied means Meta returned promotion metadata; not_returned means the read succeeded without promotion metadata; unavailable means the read failed. Only applied confirms the returned offer. Missing metadata is not proof that Ads Manager displays the requested Promotion.'
9037
+ example: not_returned
9002
9038
  Ad:
9003
9039
  type: object
9004
9040
  properties:
@@ -9159,6 +9195,10 @@ components:
9159
9195
  imageUrl: { type: string, description: Alternative image URL }
9160
9196
  videoId: { type: [string, "null"], description: "Meta video ID for VIDEO-type ads. Null for non-video ads. Callers that need an embeddable MP4 can call GET /{videoId}?fields=source with the page access token." }
9161
9197
  videoUrl: { type: [string, "null"], description: "Public Facebook watch URL for VIDEO-type ads (https://www.facebook.com/watch/?v={videoId}). Null for non-video ads." }
9198
+ promotion:
9199
+ $ref: '#/components/schemas/MetaPromotion'
9200
+ description: 'Meta offer read from the live creative on creation or GET /v1/ads/{adId}. Null when metadata is not returned or cannot be read. Requested values are never echoed as applied.'
9201
+ promotionStatus: { $ref: '#/components/schemas/MetaPromotionStatus' }
9162
9202
  creativeId: { type: [string, "null"], description: "Meta ad creative id backing this ad. Reusable via existingCreativeId on POST /v1/ads/create." }
9163
9203
  objectType: { type: string, description: "Meta creative object_type (e.g. SHARE, VIDEO, PRIVACY_CHECK_FAIL, POST_DELETED). Use this to render state-aware previews: when Meta moderation strips image/video fields, only thumbnailUrl at 64x64 is available." }
9164
9204
  objectStoryId: { type: [string, "null"], description: "Meta creative `object_story_id` (the SHARE reference). Frequently absent, because Meta omits it for SHARE creatives. Use effectiveObjectStoryId instead." }
@@ -43790,7 +43830,9 @@ paths:
43790
43830
  Duplicates a single ad via Meta's native `POST /{ad-id}/copies`. The copy is created
43791
43831
  paused. `adSetId` retargets the copy into another ad set; omitted = the source's own ad
43792
43832
  set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered
43793
- automatically (`syncAfter: false` to skip).
43833
+ automatically (`syncAfter: false` to skip). Creative settings returned by Meta,
43834
+ including explicit promotion metadata and creativeFeatures, are preserved when the
43835
+ native copy requires a creative rebuild. Metadata Meta does not return cannot be recovered.
43794
43836
  security:
43795
43837
  - bearerAuth: []
43796
43838
  parameters:
@@ -44282,9 +44324,19 @@ paths:
44282
44324
  - the creative's `effective_instagram_media_id` (Instagram side)
44283
44325
 
44284
44326
  Any of the four resolve to the same ad. Caller doesn't need a translation step.
44327
+ By default, creative.promotion and creative.creativeFeatures contain stored requested
44328
+ settings, which do not confirm platform application. With `refreshPromotion=true`,
44329
+ Meta promotion metadata is read live and exposed as `ad.creative.promotion`
44330
+ with `promotionStatus`. Only `applied` confirms an offer; `not_returned` means the
44331
+ creative read succeeded without promotion metadata, and `unavailable` means it failed.
44285
44332
  security:
44286
44333
  - bearerAuth: []
44287
44334
  parameters:
44335
+ - name: refreshPromotion
44336
+ in: query
44337
+ required: false
44338
+ schema: { type: boolean, default: false }
44339
+ description: 'Meta only. Read current promotion metadata from Meta and include promotionStatus. Omit for stored creative settings with no promotion-specific Graph call.'
44288
44340
  - name: adId
44289
44341
  in: path
44290
44342
  required: true
@@ -44300,6 +44352,7 @@ paths:
44300
44352
  type: object
44301
44353
  properties:
44302
44354
  ad: { $ref: '#/components/schemas/Ad' }
44355
+ '400': { $ref: '#/components/responses/BadRequest' }
44303
44356
  '401': { $ref: '#/components/responses/Unauthorized' }
44304
44357
  '404': { $ref: '#/components/responses/NotFound' }
44305
44358
  put:
@@ -44448,6 +44501,10 @@ paths:
44448
44501
  GET /v1/ads/creatives and ignores every other field. Meta creatives are
44449
44502
  immutable, so any change creates a new creative and repoints the ad; the old
44450
44503
  creative is retained on the ad account for historical reporting.
44504
+ `promotion` and `creativeFeatures` are Meta-only. Omitted settings are
44505
+ preserved from the live creative, including full rebuilds. Send
44506
+ `promotion: null` to remove the explicit offer from the replacement.
44507
+ A supplied creativeFeatures map overrides individual existing keys.
44451
44508
  - **TikTok**: patch-style. Pass any subset; `headline` is ignored (TikTok creatives
44452
44509
  have no headline slot). `body` becomes the in-feed `ad_text`; `linkUrl` becomes
44453
44510
  `landing_page_url`; `videoUrl` triggers a fresh upload. `description`, `videoId`
@@ -44458,6 +44515,8 @@ paths:
44458
44515
  The old creative is retained for historical reporting. `videoId` and
44459
44516
  `existingCreativeId` are Meta-only and return 400.
44460
44517
  properties:
44518
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
44519
+ creativeFeatures: { $ref: '#/components/schemas/MetaCreativeFeatures' }
44461
44520
  headline: { type: string, description: "Meta and LinkedIn (TikTok has no headline slot)" }
44462
44521
  body: { type: string }
44463
44522
  description: { type: string, maxLength: 255, description: "Link description slot (Meta `link_data.description` / `video_data.link_description`, LinkedIn creative description)." }
@@ -44468,6 +44527,10 @@ paths:
44468
44527
  videoId: { type: string, description: "Meta only. Reuse an already-uploaded ad video (from POST /v1/ads/videos or GET /v1/ads/videos) instead of re-uploading via videoUrl." }
44469
44528
  existingCreativeId: { type: string, description: "Meta only. Repoint the ad at an existing library creative (from GET /v1/ads/creatives); all other creative fields are ignored." }
44470
44529
  name: { type: string, maxLength: 255, description: "Rename the ad. Now propagated to Meta (POST /{ad-id}); non-Meta platforms return 501." }
44530
+ example:
44531
+ creative:
44532
+ promotion: null
44533
+ creativeFeatures: { auto_promotion_tag: OPT_OUT }
44471
44534
  responses:
44472
44535
  '200':
44473
44536
  description: Ad updated
@@ -45886,7 +45949,11 @@ paths:
45886
45949
  `existingCreativeId`. Provide exactly one of `imageUrl` (uploaded server-side),
45887
45950
  `imageHash` (from POST /v1/ads/images or the library list), or `carouselCards` (2-10
45888
45951
  hand-built cards). The Page (and linked Instagram account, when present) is resolved
45889
- from `accountId` as the story actor.
45952
+ from `accountId` as the story actor. `promotion` configures an explicit offer separately
45953
+ from Advantage+ `creativeFeatures`. Only when `promotion` is supplied does the response
45954
+ read the creative back from Meta;
45955
+ `promotionStatus: not_returned` means Meta accepted creation but omitted promotion
45956
+ metadata, so the requested offer is not confirmed as applied.
45890
45957
  security:
45891
45958
  - bearerAuth: []
45892
45959
  requestBody:
@@ -45920,14 +45987,23 @@ paths:
45920
45987
  description: { type: string, maxLength: 255 }
45921
45988
  callToAction: { type: string }
45922
45989
  urlTags: { type: string, description: "Appended to every outbound URL (e.g. utm_source=fb)." }
45990
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
45923
45991
  creativeFeatures:
45924
- type: object
45925
- additionalProperties: { type: string, enum: [OPT_IN, OPT_OUT] }
45926
- description: 'Advantage+ creative enhancements: partial map of Meta creative feature keys (snake_case) to enroll status, forwarded as degrees_of_freedom_spec.creative_features_spec. Unspecified features default to OPT_OUT.'
45992
+ $ref: '#/components/schemas/MetaCreativeFeatures'
45993
+ description: 'Meta only. Applied to each new creative, including standalone and attach shapes. With creatives[], these are defaults; an item replaces the whole feature map, including an empty map. auto_promotion_tag is an enhancement; an explicit offer uses promotion.'
45927
45994
  multiAdvertiser:
45928
45995
  type: string
45929
45996
  enum: [OPT_IN, OPT_OUT]
45930
45997
  description: "Meta only. Multi-advertiser ads: whether Meta may show this ad alongside other advertisers' in one unit. Meta auto-enrols since Aug 2024, so send OPT_OUT to leave. It is a top-level creative field, NOT a `creativeFeatures` key, and Meta rejects it there."
45998
+ example:
45999
+ accountId: '69fc524892b3d8e85f893e73'
46000
+ adAccountId: act_123456789
46001
+ headline: Save on your next order
46002
+ body: Use SAVE20 at checkout.
46003
+ linkUrl: https://example.com/shop
46004
+ imageUrl: https://example.com/ad.jpg
46005
+ promotion: { type: PERCENTAGE_OFF, value: 20, code: SAVE20 }
46006
+ creativeFeatures: { auto_promotion_tag: OPT_OUT }
45931
46007
  responses:
45932
46008
  '201':
45933
46009
  description: Creative created
@@ -45938,6 +46014,13 @@ paths:
45938
46014
  properties:
45939
46015
  adAccountId: { type: string }
45940
46016
  creativeId: { type: string, description: "Platform creative id, reusable via existingCreativeId." }
46017
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
46018
+ promotionStatus: { $ref: '#/components/schemas/MetaPromotionStatus' }
46019
+ example:
46020
+ adAccountId: act_123456789
46021
+ creativeId: '123456789012345'
46022
+ promotion: null
46023
+ promotionStatus: not_returned
45941
46024
  '400': { description: "Invalid input, or Meta rejected the create" }
45942
46025
  '401': { $ref: '#/components/responses/Unauthorized' }
45943
46026
  '422': { description: No Facebook Page found to act as the story actor }
@@ -47486,6 +47569,7 @@ paths:
47486
47569
  type: object
47487
47570
  required: [accountId, adAccountId, name, goal]
47488
47571
  properties:
47572
+ creativeFeatures: { $ref: '#/components/schemas/MetaCreativeFeatures' }
47489
47573
  postId: { type: string, description: Zernio post ID (provide this or platformPostId) }
47490
47574
  platformPostId: { type: string, description: Platform post ID (alternative to postId) }
47491
47575
  accountId: { type: string, description: Account ID }
@@ -47805,6 +47889,16 @@ paths:
47805
47889
  - Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting.
47806
47890
  - Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted.
47807
47891
 
47892
+ Meta accepts `promotion` and `creativeFeatures` on the single and attach shapes and
47893
+ as defaults for `creatives[]`. An item replaces the whole feature map; its `promotion`
47894
+ replaces the default offer, and `promotion: null` disables that default for the item.
47895
+ Reusing `existingCreativeId` uses the existing creative settings instead of new settings.
47896
+ Requested settings are persisted for lists, exports, and default ad-detail reads.
47897
+ Only ads supplied a `promotion` receive live readback; multi-create batches those reads
47898
+ in groups of up to 50 IDs without per-ad fallback. Inspect `ad.creative.promotionStatus` (or
47899
+ `ads[].creative.promotionStatus`). `not_returned` means Meta omitted the metadata;
47900
+ successful creation does not by itself prove the offer was applied or will display.
47901
+
47808
47902
  Per-platform required fields, budget minimums, and video-ad rules are documented on each property below.
47809
47903
 
47810
47904
  LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content "dark post" authored by a Company Page (see `organizationId`). Supported goals are engagement, traffic, awareness, and video_views (video ads use the `video` field; video_views requires a video), and traffic ads require `linkUrl`.
@@ -47871,10 +47965,10 @@ paths:
47871
47965
  billingEvent: { type: string, description: "Meta only. Explicit ad-set `billing_event`. Defaults to `IMPRESSIONS`. Forwarded verbatim to Meta, which validates compatibility with the optimization goal." }
47872
47966
  buyingType: { type: string, enum: [AUCTION, RESERVED], description: "Meta only. RESERVED = Reach & Frequency: requires `rfPredictionId` (a RESERVED prediction from /v1/ads/rf-predictions + /reserve). Budget, schedule and pricing come from the reservation, so budgetAmount/budgetType are not required and bid fields are ignored. Only the plain single-ad shape (no creatives[], adSetId, existingCampaignId or dynamicCreative)." }
47873
47967
  rfPredictionId: { type: string, description: "Meta only. The RESERVED prediction id the R&F ad set runs on (reserving mints a new id, so pass that one). Requires buyingType RESERVED." }
47968
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
47874
47969
  creativeFeatures:
47875
- type: object
47876
- additionalProperties: { type: string, enum: [OPT_IN, OPT_OUT] }
47877
- description: 'Meta only. Advantage+ creative enhancements: a partial map of Meta creative feature keys (snake_case, e.g. enhance_cta, image_brightness_and_contrast, text_optimizations) to enroll status, forwarded as degrees_of_freedom_spec.creative_features_spec. Meta validates the keys; unspecified features default to OPT_OUT. The legacy standard_enhancements bundle is deprecated by Meta and rejected.'
47970
+ $ref: '#/components/schemas/MetaCreativeFeatures'
47971
+ description: 'Meta only. Applied to each new creative, including standalone and attach shapes. With creatives[], these are defaults; an item replaces the whole feature map, including an empty map. auto_promotion_tag is an enhancement; an explicit offer uses promotion.'
47878
47972
  multiAdvertiser:
47879
47973
  type: string
47880
47974
  enum: [OPT_IN, OPT_OUT]
@@ -47970,6 +48064,12 @@ paths:
47970
48064
  required: [headline, body, linkUrl, callToAction]
47971
48065
  description: "Each creative must supply EXACTLY ONE of `imageUrl` (image creative) or `video` (video creative)."
47972
48066
  properties:
48067
+ promotion:
48068
+ $ref: '#/components/schemas/MetaPromotion'
48069
+ description: 'Overrides the top-level offer for this item. Omit to inherit; null disables the inherited offer.'
48070
+ creativeFeatures:
48071
+ $ref: '#/components/schemas/MetaCreativeFeatures'
48072
+ description: 'Replaces the entire top-level creativeFeatures map for this item. Omit to inherit; an empty map clears these defaults.'
47973
48073
  name: { type: string, maxLength: 255, description: "Exact name for this ad. Falls back to `<name> #N` (N = 1-based position)." }
47974
48074
  headline: { type: string, maxLength: 255 }
47975
48075
  body: { type: string }
@@ -47993,7 +48093,8 @@ paths:
47993
48093
  are inherited from the ad set on Meta, and passing `bidStrategy`
47994
48094
  in attach mode returns 400. To change an existing ad set's
47995
48095
  bid, use `PUT /v1/ads/ad-sets/{adSetId}`. Mutually exclusive
47996
- with `creatives[]`.
48096
+ with `creatives[]`. `dynamicCreative` returns 400 in attach mode: create
48097
+ a new dynamic ad set by omitting `adSetId` instead.
47997
48098
 
47998
48099
  The attached ad takes the full single-creative surface:
47999
48100
  `headline`/`body`/`description`/`callToAction` plus either
@@ -48300,7 +48401,10 @@ paths:
48300
48401
  (`imageUrl`, `headline`, `body`, `linkUrl`, `callToAction`) are ignored. Mutually
48301
48402
  exclusive with the `creatives[]` multi-creative shape. Exactly ONE of `imageUrls` /
48302
48403
  `videoUrls` is required (Meta allows one ad format per asset feed; sending both →
48303
- 400). Meta limits: ≤10 images or ≤10 videos, ≤5 bodies / titles / descriptions.
48404
+ 400). Limits remain 10 images or videos and 5 bodies, titles or descriptions.
48405
+ The ad set is created with `is_dynamic_creative: true`. Combining this field
48406
+ with `adSetId` returns 400: omit `adSetId` to create a new dynamic ad set.
48407
+ Multiple headlines go in `titles`; multiple primary texts go in `bodies`.
48304
48408
  properties:
48305
48409
  imageUrls:
48306
48410
  type: array
@@ -48840,10 +48944,10 @@ paths:
48840
48944
  error_subcode 1885014).
48841
48945
  productCatalogId:
48842
48946
  type: string
48843
- description: Catalog ID for catalog/Advantage+ Shopping campaigns.
48947
+ description: "Optional catalog ID. If supplied with productSetId, the set must belong to this catalog. A catalog ID cannot replace productSetId."
48844
48948
  productSetId:
48845
48949
  type: string
48846
- description: Product Set ID inside the catalog.
48950
+ description: "Meta product SET ID from GET /v1/ads/catalogs/{catalogId}/product-sets. Zernio checks that the token can read the set and its product_catalog before creation. A catalog ID or inaccessible set returns a precise 400 naming promotedObject.productSetId. A mismatch with productCatalogId names promotedObject.productCatalogId."
48847
48951
  offlineConversionDataSetId:
48848
48952
  type: string
48849
48953
  description: 'Meta only. Offline event set (dataset) to optimise toward. Post-merger these are datasets: the id is the dataset id (for pixel-backed datasets, the pixel id).'
@@ -48851,6 +48955,21 @@ paths:
48851
48955
  type: string
48852
48956
  description: 'Meta only. WhatsApp number on messaging-destination ad sets.'
48853
48957
  additionalProperties: false
48958
+ example:
48959
+ accountId: '69fc524892b3d8e85f893e73'
48960
+ adAccountId: act_123456789
48961
+ name: Autumn promotion
48962
+ goal: traffic
48963
+ budgetAmount: 5
48964
+ budgetType: daily
48965
+ status: PAUSED
48966
+ headline: Save on your next order
48967
+ body: Use SAVE20 at checkout.
48968
+ callToAction: SHOP_NOW
48969
+ linkUrl: https://example.com/shop
48970
+ imageUrl: https://example.com/ad.jpg
48971
+ promotion: { type: PERCENTAGE_OFF, value: 20, code: SAVE20 }
48972
+ creativeFeatures: { auto_promotion_tag: OPT_OUT }
48854
48973
  responses:
48855
48974
  '200':
48856
48975
  description: 'validateOnly dry-run passed, nothing was created'
@@ -50017,7 +50136,7 @@ paths:
50017
50136
  tags: ["Ad Creatives"]
50018
50137
  x-platforms: ["meta"]
50019
50138
  summary: List a catalog's product sets
50020
- description: "Lists a Meta product catalog's product sets, the unit a catalog ad promotes. Pass the chosen set as `promotedObject.productSetId` on POST /v1/ads/create with `goal: catalog_sales`."
50139
+ description: "Lists a Meta product catalog's product sets, the unit a catalog ad promotes. Pass the chosen set id, not the parent catalog id, as `promotedObject.productSetId` on POST /v1/ads/create with `goal: catalog_sales`. Creation verifies set visibility and returns 400 for a catalog id or an inaccessible set."
50021
50140
  security:
50022
50141
  - bearerAuth: []
50023
50142
  parameters:
@@ -122,7 +122,7 @@ describe 'AdCampaignsApi' do
122
122
 
123
123
  # unit tests for create_standalone_ad
124
124
  # Create standalone ad
125
- # Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, and OpenAI Ads (ChatGPT Ads). Three mutually-exclusive request shapes are selected by the body: - Legacy single-creative shape (all platforms, the default). - Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting. - Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted. Per-platform required fields, budget minimums, and video-ad rules are documented on each property below. LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content \&quot;dark post\&quot; authored by a Company Page (see &#x60;organizationId&#x60;). Supported goals are engagement, traffic, awareness, and video_views (video ads use the &#x60;video&#x60; field; video_views requires a video), and traffic ads require &#x60;linkUrl&#x60;. **Idempotency:** this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an &#x60;Idempotency-Key&#x60; header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with &#x60;Idempotent-Replayed: true&#x60;) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). Keys are scoped to your credential and expire after 24h.
125
+ # Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, and OpenAI Ads (ChatGPT Ads). Three mutually-exclusive request shapes are selected by the body: - Legacy single-creative shape (all platforms, the default). - Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting. - Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted. Meta accepts &#x60;promotion&#x60; and &#x60;creativeFeatures&#x60; on the single and attach shapes and as defaults for &#x60;creatives[]&#x60;. An item replaces the whole feature map; its &#x60;promotion&#x60; replaces the default offer, and &#x60;promotion: null&#x60; disables that default for the item. Reusing &#x60;existingCreativeId&#x60; uses the existing creative settings instead of new settings. Requested settings are persisted for lists, exports, and default ad-detail reads. Only ads supplied a &#x60;promotion&#x60; receive live readback; multi-create batches those reads in groups of up to 50 IDs without per-ad fallback. Inspect &#x60;ad.creative.promotionStatus&#x60; (or &#x60;ads[].creative.promotionStatus&#x60;). &#x60;not_returned&#x60; means Meta omitted the metadata; successful creation does not by itself prove the offer was applied or will display. Per-platform required fields, budget minimums, and video-ad rules are documented on each property below. LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content \&quot;dark post\&quot; authored by a Company Page (see &#x60;organizationId&#x60;). Supported goals are engagement, traffic, awareness, and video_views (video ads use the &#x60;video&#x60; field; video_views requires a video), and traffic ads require &#x60;linkUrl&#x60;. **Idempotency:** this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an &#x60;Idempotency-Key&#x60; header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with &#x60;Idempotent-Replayed: true&#x60;) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). Keys are scoped to your credential and expire after 24h.
126
126
  # @param create_standalone_ad_request
127
127
  # @param [Hash] opts the optional parameters
128
128
  # @option opts [String] :idempotency_key Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409.
@@ -172,7 +172,7 @@ describe 'AdCampaignsApi' do
172
172
 
173
173
  # unit tests for duplicate_ad
174
174
  # Duplicate an ad
175
- # Duplicates a single ad via Meta&#39;s native &#x60;POST /{ad-id}/copies&#x60;. The copy is created paused. &#x60;adSetId&#x60; retargets the copy into another ad set; omitted &#x3D; the source&#39;s own ad set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered automatically (&#x60;syncAfter: false&#x60; to skip).
175
+ # Duplicates a single ad via Meta&#39;s native &#x60;POST /{ad-id}/copies&#x60;. The copy is created paused. &#x60;adSetId&#x60; retargets the copy into another ad set; omitted &#x3D; the source&#39;s own ad set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered automatically (&#x60;syncAfter: false&#x60; to skip). Creative settings returned by Meta, including explicit promotion metadata and creativeFeatures, are preserved when the native copy requires a creative rebuild. Metadata Meta does not return cannot be recovered.
176
176
  # @param ad_id Zernio ad ID or platform ad ID
177
177
  # @param [Hash] opts the optional parameters
178
178
  # @option opts [String] :idempotency_key Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409. Only 2xx responses are stored, so a request that failed with a 4xx can be retried with a corrected body under the SAME key.
@@ -214,9 +214,10 @@ describe 'AdCampaignsApi' do
214
214
 
215
215
  # unit tests for get_ad
216
216
  # Get ad details
217
- # Returns an ad with its creative, targeting, status, and performance metrics. The &#x60;{adId}&#x60; path segment accepts any identifier dialect Zernio indexes for the ad: - the Zernio internal &#x60;_id&#x60; (24-char hex) - Meta&#39;s numeric &#x60;platformAdId&#x60; (the value shipped in &#x60;comment.received&#x60; webhooks as &#x60;comment.ad.id&#x60;) - the creative&#39;s &#x60;effective_object_story_id&#x60; (&#x60;{pageId}_{postId}&#x60; shape, Facebook side) - the creative&#39;s &#x60;effective_instagram_media_id&#x60; (Instagram side) Any of the four resolve to the same ad. Caller doesn&#39;t need a translation step.
217
+ # Returns an ad with its creative, targeting, status, and performance metrics. The &#x60;{adId}&#x60; path segment accepts any identifier dialect Zernio indexes for the ad: - the Zernio internal &#x60;_id&#x60; (24-char hex) - Meta&#39;s numeric &#x60;platformAdId&#x60; (the value shipped in &#x60;comment.received&#x60; webhooks as &#x60;comment.ad.id&#x60;) - the creative&#39;s &#x60;effective_object_story_id&#x60; (&#x60;{pageId}_{postId}&#x60; shape, Facebook side) - the creative&#39;s &#x60;effective_instagram_media_id&#x60; (Instagram side) Any of the four resolve to the same ad. Caller doesn&#39;t need a translation step. By default, creative.promotion and creative.creativeFeatures contain stored requested settings, which do not confirm platform application. With &#x60;refreshPromotion&#x3D;true&#x60;, Meta promotion metadata is read live and exposed as &#x60;ad.creative.promotion&#x60; with &#x60;promotionStatus&#x60;. Only &#x60;applied&#x60; confirms an offer; &#x60;not_returned&#x60; means the creative read succeeded without promotion metadata, and &#x60;unavailable&#x60; means it failed.
218
218
  # @param ad_id Zernio &#x60;_id&#x60; (hex), Meta &#x60;platformAdId&#x60; (numeric), or one of the creative&#39;s effective story/media IDs. See description for details.
219
219
  # @param [Hash] opts the optional parameters
220
+ # @option opts [Boolean] :refresh_promotion Meta only. Read current promotion metadata from Meta and include promotionStatus. Omit for stored creative settings with no promotion-specific Graph call.
220
221
  # @return [GetAd200Response]
221
222
  describe 'get_ad test' do
222
223
  it 'should work' do
@@ -34,7 +34,7 @@ describe 'AdCreativesApi' do
34
34
 
35
35
  # unit tests for create_ad_creative
36
36
  # Create a standalone creative
37
- # Creates a creative in the library WITHOUT an ad, reusable on the create endpoints via &#x60;existingCreativeId&#x60;. Provide exactly one of &#x60;imageUrl&#x60; (uploaded server-side), &#x60;imageHash&#x60; (from POST /v1/ads/images or the library list), or &#x60;carouselCards&#x60; (2-10 hand-built cards). The Page (and linked Instagram account, when present) is resolved from &#x60;accountId&#x60; as the story actor.
37
+ # Creates a creative in the library WITHOUT an ad, reusable on the create endpoints via &#x60;existingCreativeId&#x60;. Provide exactly one of &#x60;imageUrl&#x60; (uploaded server-side), &#x60;imageHash&#x60; (from POST /v1/ads/images or the library list), or &#x60;carouselCards&#x60; (2-10 hand-built cards). The Page (and linked Instagram account, when present) is resolved from &#x60;accountId&#x60; as the story actor. &#x60;promotion&#x60; configures an explicit offer separately from Advantage+ &#x60;creativeFeatures&#x60;. Only when &#x60;promotion&#x60; is supplied does the response read the creative back from Meta; &#x60;promotionStatus: not_returned&#x60; means Meta accepted creation but omitted promotion metadata, so the requested offer is not confirmed as applied.
38
38
  # @param create_ad_creative_request
39
39
  # @param [Hash] opts the optional parameters
40
40
  # @return [CreateAdCreative201Response]
@@ -124,7 +124,7 @@ describe 'AdCreativesApi' do
124
124
 
125
125
  # unit tests for list_ad_catalog_product_sets
126
126
  # List a catalog&#39;s product sets
127
- # Lists a Meta product catalog&#39;s product sets, the unit a catalog ad promotes. Pass the chosen set as &#x60;promotedObject.productSetId&#x60; on POST /v1/ads/create with &#x60;goal: catalog_sales&#x60;.
127
+ # Lists a Meta product catalog&#39;s product sets, the unit a catalog ad promotes. Pass the chosen set id, not the parent catalog id, as &#x60;promotedObject.productSetId&#x60; on POST /v1/ads/create with &#x60;goal: catalog_sales&#x60;. Creation verifies set visibility and returns 400 for a catalog id or an inaccessible set.
128
128
  # @param catalog_id Meta product catalog ID (from GET /v1/ads/catalogs)
129
129
  # @param account_id A facebook, instagram, or metaads account ID
130
130
  # @param [Hash] opts the optional parameters
@@ -51,6 +51,18 @@ describe Zernio::AdCreative do
51
51
  end
52
52
  end
53
53
 
54
+ describe 'test attribute "promotion"' do
55
+ it 'should work' do
56
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
57
+ end
58
+ end
59
+
60
+ describe 'test attribute "promotion_status"' do
61
+ it 'should work' do
62
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
63
+ end
64
+ end
65
+
54
66
  describe 'test attribute "creative_id"' do
55
67
  it 'should work' do
56
68
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -27,6 +27,16 @@ describe Zernio::BoostPostRequest do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'test attribute "creative_features"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
34
+ # validator.allowable_values.each do |value|
35
+ # expect { instance.creative_features = value }.not_to raise_error
36
+ # end
37
+ end
38
+ end
39
+
30
40
  describe 'test attribute "post_id"' do
31
41
  it 'should work' do
32
42
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -39,4 +39,16 @@ describe Zernio::CreateAdCreative201Response do
39
39
  end
40
40
  end
41
41
 
42
+ describe 'test attribute "promotion"' do
43
+ it 'should work' do
44
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
45
+ end
46
+ end
47
+
48
+ describe 'test attribute "promotion_status"' do
49
+ it 'should work' do
50
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
51
+ end
52
+ end
53
+
42
54
  end
@@ -93,6 +93,12 @@ describe Zernio::CreateAdCreativeRequest do
93
93
  end
94
94
  end
95
95
 
96
+ describe 'test attribute "promotion"' do
97
+ it 'should work' do
98
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
99
+ end
100
+ end
101
+
96
102
  describe 'test attribute "creative_features"' do
97
103
  it 'should work' do
98
104
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -27,6 +27,16 @@ describe Zernio::CreateCallAdRequest do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'test attribute "creative_features"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
34
+ # validator.allowable_values.each do |value|
35
+ # expect { instance.creative_features = value }.not_to raise_error
36
+ # end
37
+ end
38
+ end
39
+
30
40
  describe 'test attribute "account_id"' do
31
41
  it 'should work' do
32
42
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -27,6 +27,16 @@ describe Zernio::CreateMessagingAdRequest do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'test attribute "creative_features"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
34
+ # validator.allowable_values.each do |value|
35
+ # expect { instance.creative_features = value }.not_to raise_error
36
+ # end
37
+ end
38
+ end
39
+
30
40
  describe 'test attribute "account_id"' do
31
41
  it 'should work' do
32
42
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -27,6 +27,22 @@ describe Zernio::CreateStandaloneAdRequestCreativesInner do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'test attribute "promotion"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ end
34
+ end
35
+
36
+ describe 'test attribute "creative_features"' do
37
+ it 'should work' do
38
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
39
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
40
+ # validator.allowable_values.each do |value|
41
+ # expect { instance.creative_features = value }.not_to raise_error
42
+ # end
43
+ end
44
+ end
45
+
30
46
  describe 'test attribute "name"' do
31
47
  it 'should work' do
32
48
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -107,6 +107,12 @@ describe Zernio::CreateStandaloneAdRequest do
107
107
  end
108
108
  end
109
109
 
110
+ describe 'test attribute "promotion"' do
111
+ it 'should work' do
112
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
113
+ end
114
+ end
115
+
110
116
  describe 'test attribute "creative_features"' do
111
117
  it 'should work' do
112
118
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -39,6 +39,16 @@ describe Zernio::CtwaAdRequestBodyCreativesInner do
39
39
  end
40
40
  end
41
41
 
42
+ describe 'test attribute "creative_features"' do
43
+ it 'should work' do
44
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
45
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
46
+ # validator.allowable_values.each do |value|
47
+ # expect { instance.creative_features = value }.not_to raise_error
48
+ # end
49
+ end
50
+ end
51
+
42
52
  describe 'test attribute "headline"' do
43
53
  it 'should work' do
44
54
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -27,6 +27,16 @@ describe Zernio::CtwaAdRequestBody do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'test attribute "creative_features"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
34
+ # validator.allowable_values.each do |value|
35
+ # expect { instance.creative_features = value }.not_to raise_error
36
+ # end
37
+ end
38
+ end
39
+
30
40
  describe 'test attribute "account_id"' do
31
41
  it 'should work' do
32
42
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -0,0 +1,64 @@
1
+ =begin
2
+ #Zernio API
3
+
4
+ #API reference for Zernio. Authenticate with a Bearer API key. Base URL: https://zernio.com/api Versioning and deprecation: all endpoints are versioned in the URL path (current version: /v1). Breaking changes only ship in a new path version; existing versions keep working. Deprecated operations are marked 'deprecated: true' in this spec and announced in the changelog (https://zernio.com/changelog) before removal. Errors: every 4xx/5xx response is application/json with a machine-readable 'code' and a human-readable 'error' message (see the ErrorResponse schema).
5
+
6
+ The version of the OpenAPI document: 1.0.4
7
+ Contact: support@zernio.com
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.19.0
10
+
11
+ =end
12
+
13
+ require 'spec_helper'
14
+ require 'json'
15
+ require 'date'
16
+
17
+ # Unit tests for Zernio::MetaPromotion
18
+ # Automatically generated by openapi-generator (https://openapi-generator.tech)
19
+ # Please update as you see appropriate
20
+ describe Zernio::MetaPromotion do
21
+ #let(:instance) { Zernio::MetaPromotion.new }
22
+
23
+ describe 'test an instance of MetaPromotion' do
24
+ it 'should create an instance of MetaPromotion' do
25
+ # uncomment below to test the instance creation
26
+ #expect(instance).to be_instance_of(Zernio::MetaPromotion)
27
+ end
28
+ end
29
+
30
+ describe 'test attribute "type"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('String', ["AMOUNT_OFF", "FREE_RETURN", "FREE_SHIPPING", "PERCENTAGE_OFF", "PROMO_CODE"])
34
+ # validator.allowable_values.each do |value|
35
+ # expect { instance.type = value }.not_to raise_error
36
+ # end
37
+ end
38
+ end
39
+
40
+ describe 'test attribute "value"' do
41
+ it 'should work' do
42
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
43
+ end
44
+ end
45
+
46
+ describe 'test attribute "code"' do
47
+ it 'should work' do
48
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
49
+ end
50
+ end
51
+
52
+ describe 'test attribute "start_date"' do
53
+ it 'should work' do
54
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
55
+ end
56
+ end
57
+
58
+ describe 'test attribute "end_date"' do
59
+ it 'should work' do
60
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
61
+ end
62
+ end
63
+
64
+ end