diff --git a/phoenix-scala/app/models/promotion/Promotion.scala b/phoenix-scala/app/models/promotion/Promotion.scala index bf2e06a75d..322240824a 100644 --- a/phoenix-scala/app/models/promotion/Promotion.scala +++ b/phoenix-scala/app/models/promotion/Promotion.scala @@ -40,6 +40,9 @@ case class Promotion(id: Int = 0, formId: Int, commitId: Int, applyType: Promotion.ApplyType = Promotion.Auto, + name: String, + activeFrom: Option[Instant], + activeTo: Option[Instant], updatedAt: Instant = Instant.now, createdAt: Instant = Instant.now, archivedAt: Option[Instant] = None) @@ -53,10 +56,25 @@ case class Promotion(id: Int = 0, class Promotions(tag: Tag) extends ObjectHeads[Promotion](tag, "promotions") { - def applyType = column[Promotion.ApplyType]("apply_type") + def applyType = column[Promotion.ApplyType]("apply_type") + def name = column[String]("name") + def activeFrom = column[Option[Instant]]("active_from") + def activeTo = column[Option[Instant]]("active_to") def * = - (id, scope, contextId, shadowId, formId, commitId, applyType, updatedAt, createdAt, archivedAt) <> ((Promotion.apply _).tupled, Promotion.unapply) + (id, + scope, + contextId, + shadowId, + formId, + commitId, + applyType, + name, + activeFrom, + activeTo, + updatedAt, + createdAt, + archivedAt) <> ((Promotion.apply _).tupled, Promotion.unapply) } object Promotions diff --git a/phoenix-scala/app/models/promotion/PromotionCustomerGroupLink.scala b/phoenix-scala/app/models/promotion/PromotionCustomerGroupLink.scala new file mode 100644 index 0000000000..66d318f75d --- /dev/null +++ b/phoenix-scala/app/models/promotion/PromotionCustomerGroupLink.scala @@ -0,0 +1,43 @@ +package models.promotion + +import java.time.Instant + +import models.customer.{CustomerGroup, CustomerGroups} +import models.objects.ObjectHeadLinks._ +import shapeless._ +import utils.db._ +import utils.db.ExPostgresDriver.api._ + +case class PromotionCustomerGroupLink(id: Int = 0, + leftId: Int, + rightId: Int, + createdAt: Instant = Instant.now, + updatedAt: Instant = Instant.now) + extends FoxModel[PromotionCustomerGroupLink] + +class PromotionCustomerGroupLinks(tag: Tag) + extends FoxTable[PromotionCustomerGroupLink](tag, "promotion_customer_group_links") { + // FIXME: what an awful lot of duplication @michalrus + def id = column[Int]("id", O.PrimaryKey, O.AutoInc) + def leftId = column[Int]("left_id") + def rightId = column[Int]("right_id") + def createdAt = column[Instant]("created_at") + def updatedAt = column[Instant]("updated_at") + + def * = + (id, leftId, rightId, createdAt, updatedAt) <> ((PromotionCustomerGroupLink.apply _).tupled, PromotionCustomerGroupLink.unapply) + + def left = foreignKey(Promotions.tableName, leftId, Promotions)(_.id) + def right = foreignKey(CustomerGroups.tableName, rightId, CustomerGroups)(_.id) +} + +object PromotionCustomerGroupLinks + extends FoxTableQuery[PromotionCustomerGroupLink, PromotionCustomerGroupLinks]( + new PromotionCustomerGroupLinks(_)) + with ReturningId[PromotionCustomerGroupLink, PromotionCustomerGroupLinks] { + + val returningLens: Lens[PromotionCustomerGroupLink, Int] = lens[PromotionCustomerGroupLink].id + + def build(left: Promotion, right: CustomerGroup) = + PromotionCustomerGroupLink(leftId = left.id, rightId = right.id) +} diff --git a/phoenix-scala/app/payloads/PromotionPayloads.scala b/phoenix-scala/app/payloads/PromotionPayloads.scala index faeef7eeff..8a9fbaea01 100644 --- a/phoenix-scala/app/payloads/PromotionPayloads.scala +++ b/phoenix-scala/app/payloads/PromotionPayloads.scala @@ -1,5 +1,7 @@ package payloads +import java.time.Instant + import models.promotion.Promotion.ApplyType import payloads.DiscountPayloads.CreateDiscount import utils.aliases._ @@ -9,6 +11,9 @@ object PromotionPayloads { case class UpdatePromoDiscount(id: Int, attributes: Map[String, Json]) case class CreatePromotion(applyType: ApplyType, + name: String, + activeFrom: Option[Instant], + activeTo: Option[Instant], attributes: Map[String, Json], discounts: Seq[CreateDiscount], schema: Option[String] = None, diff --git a/phoenix-scala/app/services/promotion/PromotionManager.scala b/phoenix-scala/app/services/promotion/PromotionManager.scala index e649d1b3a3..16f93e649c 100644 --- a/phoenix-scala/app/services/promotion/PromotionManager.scala +++ b/phoenix-scala/app/services/promotion/PromotionManager.scala @@ -44,6 +44,9 @@ object PromotionManager { Promotion(scope = scope, contextId = context.id, applyType = payload.applyType, + name = payload.name, + activeFrom = payload.activeFrom, + activeTo = payload.activeTo, formId = ins.form.id, shadowId = ins.shadow.id, commitId = ins.commit.id)) diff --git a/phoenix-scala/app/utils/seeds/PromotionSeeds.scala b/phoenix-scala/app/utils/seeds/PromotionSeeds.scala index 4fd95ff609..ac04758b12 100644 --- a/phoenix-scala/app/utils/seeds/PromotionSeeds.scala +++ b/phoenix-scala/app/utils/seeds/PromotionSeeds.scala @@ -1,7 +1,8 @@ package utils.seeds -import scala.concurrent.ExecutionContext.Implicits.global +import java.time.Instant +import scala.concurrent.ExecutionContext.Implicits.global import models.account._ import models.objects._ import models.product.SimpleContext @@ -38,15 +39,15 @@ trait PromotionSeeds { for { context ← * <~ ObjectContexts.mustFindById404(SimpleContext.id) results ← * <~ discounts.map { discount ⇒ - val payload = createPromotion(discount.title, Promotion.Coupon) - insertPromotion(payload, discount, context) + val payload = createPromotion(Promotion.Coupon) + insertPromotion(payload, discount, context, discount.title) } } yield results - def insertPromotion(payload: CreatePromotion, discount: BaseDiscount, context: ObjectContext)( - implicit db: DB, - ac: AC, - au: AU): DbResultT[BasePromotion] = + def insertPromotion(payload: CreatePromotion, + discount: BaseDiscount, + context: ObjectContext, + name: String)(implicit db: DB, ac: AC, au: AU): DbResultT[BasePromotion] = for { scope ← * <~ Scope.resolveOverride(payload.scope) form ← * <~ ObjectForm(kind = Promotion.kind, attributes = payload.form.attributes) @@ -58,19 +59,20 @@ trait PromotionSeeds { applyType = payload.applyType, formId = ins.form.id, shadowId = ins.shadow.id, - commitId = ins.commit.id)) + commitId = ins.commit.id, + name = name, + activeFrom = Some(Instant.now), + activeTo = None)) link ← * <~ PromotionDiscountLinks.create( PromotionDiscountLink(leftId = promotion.id, rightId = discount.discountId)) } yield BasePromotion(promotion.id, ins.form.id, ins.shadow.id, payload.applyType, discount.title) - def createPromotion(name: String, applyType: Promotion.ApplyType): CreatePromotion = { - val promotionForm = BasePromotionForm(name, applyType) - val promotionShadow = BasePromotionShadow(promotionForm) - + def createPromotion(applyType: Promotion.ApplyType): CreatePromotion = { CreatePromotion( applyType = applyType, - form = CreatePromotionForm(attributes = promotionForm.form, discounts = Seq.empty), - shadow = CreatePromotionShadow(attributes = promotionShadow.shadow, discounts = Seq.empty)) + form = CreatePromotionForm(attributes = BasePromotionForm.form, discounts = Seq.empty), + shadow = + CreatePromotionShadow(attributes = BasePromotionShadow.shadow, discounts = Seq.empty)) } } diff --git a/phoenix-scala/app/utils/seeds/generators/PromotionGenerator.scala b/phoenix-scala/app/utils/seeds/generators/PromotionGenerator.scala index 6939a05902..90cbc0ac6c 100644 --- a/phoenix-scala/app/utils/seeds/generators/PromotionGenerator.scala +++ b/phoenix-scala/app/utils/seeds/generators/PromotionGenerator.scala @@ -29,35 +29,21 @@ case class SimplePromotion(promotionId: Int = 0, totalAmount: Int, applyType: Promotion.ApplyType = Promotion.Auto) -case class SimplePromotionForm(percentOff: Percent, totalAmount: Int) { +case class SimplePromotionForm() { - val (keyMap, form) = ObjectUtils.createForm(parse(s""" + val (keyMap, form) = ObjectUtils.createForm(parse(""" { - "name" : "$percentOff% off after spending $totalAmount dollars", - "storefrontName" : "$percentOff% off after spending $totalAmount dollars", - "description" : "$percentOff% off full order after spending $totalAmount dollars", - "details" : "This offer applies after you spend over $totalAmount dollars", - "activeFrom" : "${Instant.now}", - "activeTo" : null, "tags" : [] - } }""")) } case class SimplePromotionShadow(f: SimplePromotionForm) { - val shadow = ObjectUtils.newShadow( - parse(""" + val shadow = ObjectUtils.newShadow(parse(""" { - "name" : {"type": "string", "ref": "name"}, - "storefrontName" : {"type": "richText", "ref": "storefrontName"}, - "description" : {"type": "text", "ref": "description"}, - "details" : {"type": "richText", "ref": "details"}, - "activeFrom" : {"type": "date", "ref": "activeFrom"}, - "activeTo" : {"type": "date", "ref": "activeTo"}, "tags" : {"type": "tags", "ref": "tags"} }"""), - f.keyMap) + f.keyMap) } trait PromotionGenerator { @@ -78,9 +64,11 @@ trait PromotionGenerator { for { context ← * <~ ObjectContexts.mustFindById404(SimpleContext.id) promotions ← * <~ sourceData.map(source ⇒ { - val promotionForm = SimplePromotionForm(source.percentOff, source.totalAmount) + import source.{percentOff, totalAmount} + + val promotionForm = SimplePromotionForm() val promotionShadow = SimplePromotionShadow(promotionForm) - val discountForm = SimpleDiscountForm(source.percentOff, source.totalAmount) + val discountForm = SimpleDiscountForm(percentOff, totalAmount) val discountShadow = SimpleDiscountShadow(discountForm) def discountFS: FormAndShadow = { @@ -94,6 +82,13 @@ trait PromotionGenerator { val payload = CreatePromotion(applyType = source.applyType, + name = + s"$percentOff% off after spending $totalAmount dollars", + // "storefrontName" : "$percentOff% off after spending $totalAmount dollars", + // "description" : "$percentOff% off full order after spending $totalAmount dollars", + // "details" : "This offer applies after you spend over $totalAmount dollars", + activeFrom = Some(Instant.now), + activeTo = None, attributes = promotionFS.toPayload, discounts = Seq(CreateDiscount(attributes = discountFS.toPayload))) diff --git a/phoenix-scala/app/utils/seeds/package.scala b/phoenix-scala/app/utils/seeds/package.scala index e7589ba3d1..b77c98f047 100644 --- a/phoenix-scala/app/utils/seeds/package.scala +++ b/phoenix-scala/app/utils/seeds/package.scala @@ -53,35 +53,14 @@ package object seeds { applyType: Promotion.ApplyType, title: String) - case class BasePromotionForm(name: String, applyType: Promotion.ApplyType) { - - val (keyMap, form) = ObjectUtils.createForm(parse(s""" - { - "name" : "$name", - "storefrontName" : "$name", - "description" : "$name", - "details" : "", - "activeFrom" : "${Instant.now}", - "activeTo" : null, - "tags" : [] - } - }""")) + case object BasePromotionForm { + val (keyMap, form) = ObjectUtils.createForm(parse(s"""{ "tags" : [] }""")) } - case class BasePromotionShadow(f: BasePromotionForm) { + case object BasePromotionShadow { - val shadow = ObjectUtils.newShadow( - parse(""" - { - "name" : {"type": "string", "ref": "name"}, - "storefrontName" : {"type": "richText", "ref": "storefrontName"}, - "description" : {"type": "text", "ref": "description"}, - "details" : {"type": "richText", "ref": "details"}, - "activeFrom" : {"type": "date", "ref": "activeFrom"}, - "activeTo" : {"type": "date", "ref": "activeTo"}, - "tags" : {"type": "tags", "ref": "tags"} - }"""), - f.keyMap) + val shadow = ObjectUtils + .newShadow(parse("""{"tags" : {"type": "tags", "ref": "tags"}}"""), BasePromotionForm.keyMap) } case class BaseCoupon(formId: Int = 0, shadowId: Int = 0, promotionId: Int) diff --git a/phoenix-scala/resources/object_schemas/promotion.json b/phoenix-scala/resources/object_schemas/promotion.json index ed1eefb363..875ebb7efe 100644 --- a/phoenix-scala/resources/object_schemas/promotion.json +++ b/phoenix-scala/resources/object_schemas/promotion.json @@ -5,28 +5,7 @@ "properties": { "attributes": { "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1 - }, - "customerGroupIds": { - "type": "array", - "items": { - "type": "number" - }, - "uniqueItems": true - }, - "activeFrom": { - "type": ["string", "null"], - "format": "date-time" - }, - "activeTo": { - "type": ["string", "null"], - "format": "date-time" - } - }, - "required": ["name"] + "properties": {} }, "discounts": { "type": "array", diff --git a/phoenix-scala/sql/V4.117__less_enlightened_promos.sql b/phoenix-scala/sql/V4.117__less_enlightened_promos.sql new file mode 100644 index 0000000000..605a60f5ba --- /dev/null +++ b/phoenix-scala/sql/V4.117__less_enlightened_promos.sql @@ -0,0 +1,82 @@ +create table promotion_customer_group_links ( + id serial primary key + , left_id integer not null references promotions(id) on update restrict on delete restrict + , right_id integer not null references customer_groups(id) on update restrict on delete restrict + , created_at generic_timestamp + , updated_at generic_timestamp + , foreign key (left_id) references promotions(id) on update restrict on delete restrict + , foreign key (right_id) references customer_groups(id) on update restrict on delete restrict + ); + +create index promotion_customer_group_link_left_idx on promotion_customer_group_links (left_id); +create index promotion_customer_group_link_right_idx on promotion_customer_group_links (right_id); + +insert into promotion_customer_group_links + ( left_id + , right_id + , created_at + , updated_at + ) + select + promotions.id + , jsonb_array_elements_text( + illuminate_obj(object_forms, object_shadows, 'customerGroupIds') + ) :: integer + , now() + , now() + from + promotions + inner join object_forms on (object_forms.id = promotions.form_id) + inner join object_shadows on (object_shadows.id = promotions.shadow_id) + ; + +------------------------------------------------------------------------------- + +alter table promotions + add column name text + , add column active_from timestamp + , add column active_to timestamp + ; + +update promotions +set + name = q.name + , active_from = q.active_from + , active_to = q.active_to +from + (select + promotions.id + , illuminate_obj(object_forms, object_shadows, 'name') as name + , illuminate_text(object_forms, object_shadows, 'activeFrom') :: timestamp as active_from + , illuminate_text(object_forms, object_shadows, 'activeTo') :: timestamp as active_to + from + promotions + inner join object_forms on (object_forms.id = promotions.form_id) + inner join object_shadows on (object_shadows.id = promotions.shadow_id) + ) as q +where + promotions.id = q.id + ; + +update object_shadows +set + attributes = q.attributes +from + (select + object_shadows.id as id + , object_shadows.attributes + - 'name' + - 'details' + - 'description' + - 'storefrontName' + - 'customerGroupIds' + - 'activeFrom' + - 'activeTo' + as attributes + from + object_shadows + inner join promotions on (object_shadows.id = promotions.shadow_id) + ) as q +where + object_shadows.id = q.id; + ; diff --git a/phoenix-scala/test/integration/PromotionsIntegrationTest.scala b/phoenix-scala/test/integration/PromotionsIntegrationTest.scala index 26c622bd7e..e57c05fa12 100644 --- a/phoenix-scala/test/integration/PromotionsIntegrationTest.scala +++ b/phoenix-scala/test/integration/PromotionsIntegrationTest.scala @@ -150,12 +150,13 @@ class PromotionsIntegrationTest CreateDiscount(attributes = discountAttrs) } - val promoAttrs = - Map("name" → tv("testyPromo"), "storefrontName" → tv("
Testy promo
", "richText")) - CreatePromotion(applyType = Promotion.Coupon, + name = "testyPromo", + // "storefrontName" → tv("Testy promo
", "richText") + activeFrom = Some(Instant.now), // TODO: really? Not `None`? @michalrus + activeTo = None, discounts = Seq(discountPayload), - attributes = promoAttrs ++ extraPromoAttrs) + attributes = extraPromoAttrs) } promotionsApi.create(promotionPayload).as[PromotionResponse.Root].id diff --git a/phoenix-scala/test/integration/testutils/fixtures/PromotionFixtures.scala b/phoenix-scala/test/integration/testutils/fixtures/PromotionFixtures.scala index f2b870079b..07ad245b92 100644 --- a/phoenix-scala/test/integration/testutils/fixtures/PromotionFixtures.scala +++ b/phoenix-scala/test/integration/testutils/fixtures/PromotionFixtures.scala @@ -1,5 +1,7 @@ package testutils.fixtures +import java.time.Instant + import failures.NotFoundFailure404 import models.account.User import models.promotion.{Promotion, Promotions} @@ -41,12 +43,15 @@ trait PromotionFixtures extends TestFixtureBase { val discountAttributes = makeDiscountAttrs("orderTotalAmount", "totalAmount" → JInt(totalAmount * 100)) - val promoAttributes = Map[String, Json]("name" → tv("donkey promo")) + val promoAttributes = Map.empty[String, Json] - val promoPayload = CreatePromotion(applyType = Promotion.Coupon, - attributes = promoAttributes, - discounts = - Seq(CreateDiscount(attributes = discountAttributes))) + val promoPayload = CreatePromotion( + applyType = Promotion.Coupon, + name = "donkey promo", + activeFrom = Some(Instant.now), // TODO: really? Not `None`? @michalrus + activeTo = None, + attributes = promoAttributes, + discounts = Seq(CreateDiscount(attributes = discountAttributes))) val (promoRoot: PromotionResponse.Root, promotion: Promotion) = createPromotionFromPayload( promoPayload) diff --git a/phoenix-scala/test/integration/testutils/fixtures/api/PromotionPayloadBuilder.scala b/phoenix-scala/test/integration/testutils/fixtures/api/PromotionPayloadBuilder.scala index 221dcb7b72..72a2e4abc8 100644 --- a/phoenix-scala/test/integration/testutils/fixtures/api/PromotionPayloadBuilder.scala +++ b/phoenix-scala/test/integration/testutils/fixtures/api/PromotionPayloadBuilder.scala @@ -28,11 +28,10 @@ object PromotionPayloadBuilder { ) CreatePromotion(applyType = applyType, - attributes = Map( - "name" → tv(faker.Lorem.sentence(1)), - "activeFrom" → tv(Instant.now, "datetime"), - "activeTo" → tv(JNull, "datetime") - ) ++ extraAttrs, + name = faker.Lorem.sentence(1), + activeFrom = Some(Instant.now), + activeTo = None, + attributes = extraAttrs, discounts = Seq(CreateDiscount(discountAttrs))) }