From 7ef5f03eb3fd0295408fff168bd5399a1b0bc06a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 27 May 2026 23:23:07 -0400 Subject: [PATCH] feat(spot-card-exchange): /spot_card_exchange/{top,exchange} + SpotPoints currency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final shop family. Schema additions: - ViewerCurrency.SpotPoints (ulong) — new currency column on Viewers. - SpotCardExchangeEntry — catalog (distinct from the pre-existing SpotCardEntry, which is the /load/index rental-cost concept). - ViewerSpotCardExchange — standalone composite-PK table tracking (viewer, card, exchanged_at, is_pre_release_snapshot). Standalone avoids cartesian-explode on viewer-graph reads. RewardGrantService gains a SpotCardPoint=12 currency case mirroring the RedEther/Crystal pattern. Doc comment refreshed; SpotCard=11 and SpotCardOnlyLatestCardPack=13 remain unimplemented with explanatory NotSupportedException — captures show emitters always use Card=5 with the spot-card-specific id. Controller: - /top: emits exactly 9 clan buckets [{"1": [cards]}, ...] matching prod's arbitrary single-key shape. exchange_status per-card (0= available, 1=already-exchanged, 2=LimitOver after pre-release cap). pre_relase_info WIRE TYPO PRESERVED ("relase" not "release"). - /exchange: server-authoritative price (client-supplied exchange_point ignored); debits SpotPoints with post-state-total reward_list entry; grants card via RewardGrantService.ApplyAsync (cosmetic cascade included); persists ViewerSpotCardExchange row. Insufficient points / already-exchanged / pre-release-limit all return 400 without partial state. LoadController now populates /load/index spot_point from viewer.Currency.SpotPoints (was always 0). PreReleaseLimit hardcoded to 2 matching capture; promote to GameConfig when captures show variance. 504 tests pass (was 496; +8 spot-card-exchange tests). Co-Authored-By: Claude Opus 4.7 --- .../Data/seeds/spot-card-exchange.json | 3733 +++++++++++++++++ .../Importers/SpotCardExchangeImporter.cs | 55 + .../Models/Seed/SpotCardExchangeSeed.cs | 12 + SVSim.Bootstrap/Program.cs | 1 + ...0528030221_AddSpotCardExchange.Designer.cs | 3685 ++++++++++++++++ .../20260528030221_AddSpotCardExchange.cs | 74 + .../Migrations/SVSimDbContextModelSnapshot.cs | 58 + .../Models/SpotCardExchangeEntry.cs | 30 + SVSim.Database/Models/ViewerCurrency.cs | 7 + .../Models/ViewerSpotCardExchange.cs | 16 + SVSim.Database/SVSimDbContext.cs | 8 + SVSim.Database/Services/RewardGrantService.cs | 17 +- .../Controllers/LoadController.cs | 1 + .../Controllers/SpotCardExchangeController.cs | 192 + .../SpotCardExchangeRequest.cs | 22 + .../SpotCardExchangeResponse.cs | 18 + .../SpotCardExchangeTopResponse.cs | 88 + .../SpotCardExchangeControllerTests.cs | 216 + .../SpotCardExchangeImporterTests.cs | 70 + .../Infrastructure/SVSimTestFactory.cs | 1 + 20 files changed, 8298 insertions(+), 6 deletions(-) create mode 100644 SVSim.Bootstrap/Data/seeds/spot-card-exchange.json create mode 100644 SVSim.Bootstrap/Importers/SpotCardExchangeImporter.cs create mode 100644 SVSim.Bootstrap/Models/Seed/SpotCardExchangeSeed.cs create mode 100644 SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.Designer.cs create mode 100644 SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.cs create mode 100644 SVSim.Database/Models/SpotCardExchangeEntry.cs create mode 100644 SVSim.Database/Models/ViewerSpotCardExchange.cs create mode 100644 SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Requests/SpotCardExchange/SpotCardExchangeRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeTopResponse.cs create mode 100644 SVSim.UnitTests/Controllers/SpotCardExchangeControllerTests.cs create mode 100644 SVSim.UnitTests/Importers/SpotCardExchangeImporterTests.cs diff --git a/SVSim.Bootstrap/Data/seeds/spot-card-exchange.json b/SVSim.Bootstrap/Data/seeds/spot-card-exchange.json new file mode 100644 index 0000000..f7330cc --- /dev/null +++ b/SVSim.Bootstrap/Data/seeds/spot-card-exchange.json @@ -0,0 +1,3733 @@ +[ + { + "card_id": 113041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113144010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113242010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113344010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 113841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10013, + "is_pre_release": false + }, + { + "card_id": 114041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 114844010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10014, + "is_pre_release": false + }, + { + "card_id": 115041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 115841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10015, + "is_pre_release": false + }, + { + "card_id": 116041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 116841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10016, + "is_pre_release": false + }, + { + "card_id": 117041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 117841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10017, + "is_pre_release": false + }, + { + "card_id": 118041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 118841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10018, + "is_pre_release": false + }, + { + "card_id": 119041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119044010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 119841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10019, + "is_pre_release": false + }, + { + "card_id": 120041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120044010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 120841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10020, + "is_pre_release": false + }, + { + "card_id": 121041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121044010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 121841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10021, + "is_pre_release": false + }, + { + "card_id": 122041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122044010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122244010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 122844010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10022, + "is_pre_release": false + }, + { + "card_id": 123041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123244010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 123841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10023, + "is_pre_release": false + }, + { + "card_id": 124041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 124841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10024, + "is_pre_release": false + }, + { + "card_id": 125041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125041030, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 125844010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10025, + "is_pre_release": false + }, + { + "card_id": 126041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 126841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10026, + "is_pre_release": false + }, + { + "card_id": 127041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127041030, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 127841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10027, + "is_pre_release": false + }, + { + "card_id": 128041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 128841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10028, + "is_pre_release": false + }, + { + "card_id": 129041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129041030, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 129841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10029, + "is_pre_release": false + }, + { + "card_id": 130041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130743010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 130841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10030, + "is_pre_release": false + }, + { + "card_id": 131041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131044010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 131841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10031, + "is_pre_release": false + }, + { + "card_id": 132041010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132041020, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132041030, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132044010, + "class": 0, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132141010, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132141020, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132141030, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132141040, + "class": 1, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132241010, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132241020, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132241030, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132241040, + "class": 2, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132341010, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132341020, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132341030, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132341040, + "class": 3, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132441010, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132441020, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132441030, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132441040, + "class": 4, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132541010, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132541020, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132541030, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132541040, + "class": 5, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132641010, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132641020, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132641030, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132641040, + "class": 6, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132741010, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132741020, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132741030, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132741040, + "class": 7, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132841010, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132841020, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132841030, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + }, + { + "card_id": 132841040, + "class": 8, + "exchange_point": 3500, + "ts_rotation_id": 10032, + "is_pre_release": false + } +] diff --git a/SVSim.Bootstrap/Importers/SpotCardExchangeImporter.cs b/SVSim.Bootstrap/Importers/SpotCardExchangeImporter.cs new file mode 100644 index 0000000..182fcf0 --- /dev/null +++ b/SVSim.Bootstrap/Importers/SpotCardExchangeImporter.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using SVSim.Bootstrap.Models.Seed; +using SVSim.Database; +using SVSim.Database.Models; + +namespace SVSim.Bootstrap.Importers; + +/// +/// Idempotent upsert of the spot card exchange catalog from seeds/spot-card-exchange.json. +/// Source is the wire /spot_card_exchange/top response, extracted via +/// data_dumps/extract/extract-spot-card-exchange.py. Rows missing from the seed are +/// LEFT INTACT. +/// +public class SpotCardExchangeImporter +{ + public async Task ImportAsync(SVSimDbContext context, string seedDir) + { + string path = Path.Combine(seedDir, "spot-card-exchange.json"); + var seed = SeedLoader.LoadList(path); + if (seed.Count == 0) + { + Console.WriteLine("[SpotCardExchangeImporter] No seed rows; skipping."); + return 0; + } + + var existing = await context.SpotCardExchangeCatalog.ToDictionaryAsync(e => e.Id); + int created = 0, updated = 0; + + foreach (var s in seed) + { + if (s.CardId == 0) continue; + + var entry = existing.TryGetValue(s.CardId, out var ex) + ? ex : new SpotCardExchangeEntry { Id = s.CardId }; + + entry.ClassId = s.ClassId; + entry.ExchangePoint = s.ExchangePoint; + entry.TsRotationId = s.TsRotationId; + entry.IsPreRelease = s.IsPreRelease; + entry.IsEnabled = true; + + if (ex is null) + { + context.SpotCardExchangeCatalog.Add(entry); + existing[s.CardId] = entry; + created++; + } + else updated++; + } + + await context.SaveChangesAsync(); + Console.WriteLine($"[SpotCardExchangeImporter] +{created}/~{updated}"); + return created + updated; + } +} diff --git a/SVSim.Bootstrap/Models/Seed/SpotCardExchangeSeed.cs b/SVSim.Bootstrap/Models/Seed/SpotCardExchangeSeed.cs new file mode 100644 index 0000000..9d2cb25 --- /dev/null +++ b/SVSim.Bootstrap/Models/Seed/SpotCardExchangeSeed.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace SVSim.Bootstrap.Models.Seed; + +public sealed class SpotCardExchangeSeed +{ + [JsonPropertyName("card_id")] public long CardId { get; set; } + [JsonPropertyName("class")] public int ClassId { get; set; } + [JsonPropertyName("exchange_point")] public int ExchangePoint { get; set; } + [JsonPropertyName("ts_rotation_id")] public long TsRotationId { get; set; } + [JsonPropertyName("is_pre_release")] public bool IsPreRelease { get; set; } +} diff --git a/SVSim.Bootstrap/Program.cs b/SVSim.Bootstrap/Program.cs index e2d1290..92e501a 100644 --- a/SVSim.Bootstrap/Program.cs +++ b/SVSim.Bootstrap/Program.cs @@ -101,6 +101,7 @@ public static class Program await new SleeveShopImporter().ImportAsync(context, opts.SeedDir); await new ItemPurchaseImporter().ImportAsync(context, opts.SeedDir); await new LeaderSkinShopImporter().ImportAsync(context, opts.SeedDir); + await new SpotCardExchangeImporter().ImportAsync(context, opts.SeedDir); var puzzleImporter = new PuzzleImporter(); await puzzleImporter.ImportGroupsAsync(context, opts.SeedDir); await puzzleImporter.ImportPuzzlesAsync(context, opts.SeedDir); diff --git a/SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.Designer.cs b/SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.Designer.cs new file mode 100644 index 0000000..ace543b --- /dev/null +++ b/SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.Designer.cs @@ -0,0 +1,3685 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SVSim.Database; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + [DbContext(typeof(SVSimDbContext))] + [Migration("20260528030221_AddSpotCardExchange")] + partial class AddSpotCardExchange + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("ShortUdidSequence") + .StartsAt(400000000L); + + modelBuilder.Entity("DegreeEntryViewer", b => + { + b.Property("DegreesId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("DegreesId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("DegreeEntryViewer"); + }); + + modelBuilder.Entity("EmblemEntryViewer", b => + { + b.Property("EmblemsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("EmblemsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("EmblemEntryViewer"); + }); + + modelBuilder.Entity("LeaderSkinEntryViewer", b => + { + b.Property("LeaderSkinsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("LeaderSkinsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("LeaderSkinEntryViewer"); + }); + + modelBuilder.Entity("MyPageBackgroundEntryViewer", b => + { + b.Property("MyPageBackgroundsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("MyPageBackgroundsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("MyPageBackgroundEntryViewer"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.SpecialBattleSetting", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BanishEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClassDestroyEffectOverride") + .HasColumnType("integer"); + + b.Property("EnemyAttachSkill") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnemyStartLife") + .HasColumnType("integer"); + + b.Property("EnemyStartPp") + .HasColumnType("integer"); + + b.Property("IdOverrideInBattleLog") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PlayerAttachSkill") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerFirstTurn") + .HasColumnType("integer"); + + b.Property("PlayerStartLife") + .HasColumnType("integer"); + + b.Property("PlayerStartPp") + .HasColumnType("integer"); + + b.Property("ResultSkip") + .HasColumnType("integer"); + + b.Property("SpecialTokenDrawEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenDrawEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("VsEffectOverride") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SpecialBattleSettings"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b => + { + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("Battle3dFieldId") + .HasColumnType("integer"); + + b.Property("BattleExists") + .HasColumnType("boolean"); + + b.Property("BgFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BgmId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterClearTextId") + .HasColumnType("text"); + + b.Property("ChapterEffectPath") + .HasColumnType("text"); + + b.Property("ChapterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("EnemyAiId") + .HasColumnType("integer"); + + b.Property("EnemyCharaId") + .HasColumnType("integer"); + + b.Property("EnemyClass") + .HasColumnType("integer"); + + b.Property("IsCameraMovable") + .HasColumnType("integer"); + + b.Property("IsMaintenanceChapter") + .HasColumnType("boolean"); + + b.Property("IsPlayAnotherEndAppearanceAnimation") + .HasColumnType("boolean"); + + b.Property("IsReleasedAnotherEnd") + .HasColumnType("boolean"); + + b.Property("IsSkipEnabled") + .HasColumnType("boolean"); + + b.Property("NextChapterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReleasePoint") + .HasColumnType("integer"); + + b.Property("RequiredChapterId") + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("integer"); + + b.Property("SelectionDisplayPosition") + .HasColumnType("text"); + + b.Property("SelectionTextId") + .HasColumnType("text"); + + b.Property("ShowCoordinate") + .HasColumnType("integer"); + + b.Property("ShowSubtitles") + .HasColumnType("integer"); + + b.Property("SpecialBattleSettingId") + .HasColumnType("integer"); + + b.Property("UnlockText") + .HasColumnType("text"); + + b.Property("XCoordinate") + .HasColumnType("numeric"); + + b.Property("YCoordinate") + .HasColumnType("numeric"); + + b.HasKey("StoryId"); + + b.HasIndex("NextChapterId"); + + b.HasIndex("SpecialBattleSettingId"); + + b.HasIndex("SectionId", "CharaId", "ChapterId"); + + b.ToTable("StoryChapters"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AllStoryOrderId") + .HasColumnType("integer"); + + b.Property("BackGroundId") + .HasColumnType("integer"); + + b.Property("ChapterSelectType") + .HasColumnType("integer"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLeaderSelect") + .HasColumnType("boolean"); + + b.Property("IsPlayAnotherEndAppearanceAnimation") + .HasColumnType("boolean"); + + b.Property("IsSpoiler") + .HasColumnType("integer"); + + b.Property("IsUnderMaintenance") + .HasColumnType("boolean"); + + b.Property("NameTextKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("SpoilerMessage") + .IsRequired() + .HasColumnType("text"); + + b.Property("StoryApiType") + .HasColumnType("integer"); + + b.Property("StoryTypeOverwrite") + .HasColumnType("integer"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("StorySections"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryWorld", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("PanelImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RibbonText") + .IsRequired() + .HasColumnType("text"); + + b.Property("TitleTextKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("StoryWorlds"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryBranchUnlock", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("UnlockedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "StoryId"); + + b.ToTable("ViewerStoryBranchUnlocks"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryProgress", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsFinish") + .HasColumnType("boolean"); + + b.Property("IsSkipped") + .HasColumnType("boolean"); + + b.Property("SkippedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "StoryId"); + + b.ToTable("ViewerStoryProgress"); + }); + + modelBuilder.Entity("SVSim.Database.Models.AchievementCatalogEntry", b => + { + b.Property("AchievementType") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("AchievementType", "Level"); + + b.HasIndex("AchievementType"); + + b.HasIndex("EventType", "EventArg"); + + b.ToTable("AchievementCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Cost") + .HasColumnType("numeric(20,0)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Enable") + .HasColumnType("integer"); + + b.Property("FormatInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsJoin") + .HasColumnType("boolean"); + + b.Property("Mode") + .HasColumnType("integer"); + + b.Property("RupyCost") + .HasColumnType("numeric(20,0)"); + + b.Property("TicketCost") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ArenaSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.AvatarAbilityEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Ability") + .IsRequired() + .HasColumnType("text"); + + b.Property("AbilityCost") + .IsRequired() + .HasColumnType("text"); + + b.Property("AbilityDesc") + .IsRequired() + .HasColumnType("text"); + + b.Property("BattleStartFirstPlayerTurnBp") + .HasColumnType("integer"); + + b.Property("BattleStartMaxLife") + .HasColumnType("integer"); + + b.Property("BattleStartSecondPlayerTurnBp") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("PassiveAbility") + .IsRequired() + .HasColumnType("text"); + + b.Property("PassiveAbilityDesc") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AvatarAbilities"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BannerEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ChangeTime") + .HasColumnType("integer"); + + b.Property("Click") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImagePaths") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Banners"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassLevelEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("RequiredPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("BattlePassLevels"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassMonthlyMissionEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattlePassPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("Month") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Month"); + + b.HasIndex("Year", "Month", "OrderNum") + .IsUnique(); + + b.ToTable("BattlePassMonthlyMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAppealExclusion") + .HasColumnType("boolean"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Track") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Track", "Level") + .IsUnique(); + + b.ToTable("BattlePassRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassSeasonEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CanPurchase") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceCrystal") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("StartDate", "EndDate"); + + b.ToTable("BattlePassSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlefieldEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsOpen") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Battlefields"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("FeaturedCardId") + .HasColumnType("bigint"); + + b.Property("IntroPriceCrystal") + .HasColumnType("integer"); + + b.Property("IntroPriceRupy") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LeaderId") + .HasColumnType("integer"); + + b.Property("ProductNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PurchaseNumMax") + .HasColumnType("integer"); + + b.Property("RegularPriceCrystal") + .HasColumnType("integer"); + + b.Property("RegularPriceRupy") + .HasColumnType("integer"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("BuildDeckProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DrumrollPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("IntroKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("NameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderIndex") + .HasColumnType("integer"); + + b.Property("TitlePath") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BuildDeckSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b => + { + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("CosmeticId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("CardId", "Type", "CosmeticId"); + + b.HasIndex("CardId"); + + b.ToTable("CardCosmeticRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Classes"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassExpEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("NecessaryExp") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClassExpCurve"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ColosseumConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardPoolName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColosseumId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColosseumName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAllCardEnabled") + .HasColumnType("integer"); + + b.Property("IsColosseumPeriod") + .HasColumnType("boolean"); + + b.Property("IsDisplayTips") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsNormalTwoPick") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsRoundPeriod") + .HasColumnType("boolean"); + + b.Property("IsSpecialMode") + .IsRequired() + .HasColumnType("text"); + + b.Property("NowRound") + .IsRequired() + .HasColumnType("text"); + + b.Property("SalesPeriodInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TipsId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Colosseums"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DailyLoginBonusEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BonusData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("BonusId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("DailyLoginBonuses"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DefaultDeckEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardIdArray") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("DefaultDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DegreeEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Degrees"); + }); + + modelBuilder.Entity("SVSim.Database.Models.EmblemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Emblems"); + }); + + modelBuilder.Entity("SVSim.Database.Models.FeatureMaintenanceEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("FeatureMaintenances"); + }); + + modelBuilder.Entity("SVSim.Database.Models.GameConfigSection", b => + { + b.Property("SectionName") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ValueJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("SectionName"); + + b.ToTable("GameConfigs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ItemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ThumbnailPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ItemPurchaseCatalogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsMonthlyReset") + .HasColumnType("boolean"); + + b.Property("PurchaseItemId") + .HasColumnType("bigint"); + + b.Property("PurchaseItemNum") + .HasColumnType("integer"); + + b.Property("PurchaseItemType") + .HasColumnType("integer"); + + b.Property("PurchaseLimit") + .HasColumnType("integer"); + + b.Property("PurchaseName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequireItemId") + .HasColumnType("bigint"); + + b.Property("RequireItemNum") + .HasColumnType("integer"); + + b.Property("RequireItemType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemPurchaseCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EmoteId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.ToTable("LeaderSkins"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CvNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IntroductionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("ProductNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.Property("SinglePriceCrystal") + .HasColumnType("integer"); + + b.Property("SinglePriceRupy") + .HasColumnType("integer"); + + b.Property("SinglePriceTicket") + .HasColumnType("integer"); + + b.Property("TicketItemId") + .HasColumnType("bigint"); + + b.Property("TicketNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("LeaderSkinShopProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("SetCompletionRewardStatus") + .HasColumnType("integer"); + + b.Property("SetPriceCrystal") + .HasColumnType("integer"); + + b.Property("SetPriceRupy") + .HasColumnType("integer"); + + b.Property("SetPriceTicket") + .HasColumnType("integer"); + + b.Property("SetPriceTicketId") + .HasColumnType("bigint"); + + b.Property("SetSalesStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("LeaderSkinShopSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LoadingExclusionCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LoadingExclusionCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MaintenanceCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MaintenanceCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MasterPointRankingPeriodEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BeginTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("NecessaryScore") + .HasColumnType("bigint"); + + b.Property("PeriodNum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MasterPointRankingPeriods"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MissionCatalogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BattlePassPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultFlag") + .HasColumnType("boolean"); + + b.Property("EndTime") + .HasColumnType("bigint"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("LotType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("LotType"); + + b.HasIndex("EventType", "EventArg"); + + b.ToTable("MissionCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyPageBackgroundEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MyPageBackgrounds"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyRotationAbilityEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AbilityId") + .HasColumnType("integer"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MyRotationAbilities"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyRotationSettingEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AbilitiesCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardSetIdsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ReprintedCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RestrictedCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RotationId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MyRotationSettings"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BasePackId") + .HasColumnType("integer"); + + b.Property("CommenceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CompleteDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("GachaDetail") + .IsRequired() + .HasColumnType("text"); + + b.Property("GachaType") + .HasColumnType("integer"); + + b.Property("IsHide") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("OpenCountLimit") + .HasColumnType("integer"); + + b.Property("OverrideDrawEffectPackId") + .HasColumnType("integer"); + + b.Property("OverrideUiEffectPackId") + .HasColumnType("integer"); + + b.Property("PackCategory") + .HasColumnType("integer"); + + b.Property("PosterType") + .HasColumnType("integer"); + + b.Property("SalesPeriodTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("SpecialSleeveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Packs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PaymentItemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ChargeCrystalNum") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FreeCrystalNum") + .HasColumnType("integer"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsResaleProduct") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("PurchaseLimit") + .HasColumnType("integer"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("ResaleStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SpecialShopFlag") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StoreProductId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PaymentItems"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PracticeOpponentEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AiDeckLevel") + .HasColumnType("integer"); + + b.Property("AiLogicLevel") + .HasColumnType("integer"); + + b.Property("AiMaxLife") + .HasColumnType("integer"); + + b.Property("Battle3dFieldId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DegreeId") + .HasColumnType("integer"); + + b.Property("IsCampaignPractice") + .HasColumnType("boolean"); + + b.Property("IsMaintenance") + .HasColumnType("boolean"); + + b.Property("PracticeId") + .HasColumnType("integer"); + + b.Property("TextId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PracticeOpponents"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PreReleaseInfo", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardMasterId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCardMasterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisplayEndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FreeMatchStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRotationFreeMatchTerm") + .HasColumnType("boolean"); + + b.Property("LatestReprintedBaseCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("NextCardSetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreReleaseCardMasterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreReleaseId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReprintedBaseCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RotationCardSetIdList") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("PreReleaseInfos"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("IsAdditional") + .HasColumnType("boolean"); + + b.Property("IsPlayable") + .HasColumnType("boolean"); + + b.Property("PuzzleDifficulty") + .HasColumnType("integer"); + + b.Property("PuzzleId") + .HasColumnType("integer"); + + b.Property("ReleaseConditionTextId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("Puzzles"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BasicTitleTextId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DifficultyNameListJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PuzzleCharaId") + .HasColumnType("integer"); + + b.Property("PuzzleMasterId") + .HasColumnType("integer"); + + b.Property("SortType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PuzzleGroups"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleMissionEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AchievedMessage") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignCommenceTime") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("TargetPuzzleGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PuzzleMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.RankInfoEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AccumulateMasterPoint") + .HasColumnType("integer"); + + b.Property("AccumulatePoint") + .HasColumnType("integer"); + + b.Property("BaseAddBp") + .HasColumnType("integer"); + + b.Property("BaseDropBp") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPromotionWar") + .HasColumnType("integer"); + + b.Property("LoseBonus") + .HasColumnType("double precision"); + + b.Property("LowerLimitPoint") + .HasColumnType("integer"); + + b.Property("MatchCount") + .HasColumnType("integer"); + + b.Property("MaxLoseBonus") + .HasColumnType("integer"); + + b.Property("MaxWinBonus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NecessaryPoint") + .HasColumnType("integer"); + + b.Property("NecessaryWin") + .HasColumnType("integer"); + + b.Property("ResetLose") + .HasColumnType("integer"); + + b.Property("StreakBonusPt") + .HasColumnType("integer"); + + b.Property("WinBonus") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("RankInfo"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ReprintedCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ReprintedCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SealedConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CrystalCost") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckUsingNumMin") + .HasColumnType("integer"); + + b.Property("Enable") + .HasColumnType("integer"); + + b.Property("IsDeckCodeMaintenance") + .HasColumnType("boolean"); + + b.Property("IsJoin") + .HasColumnType("boolean"); + + b.Property("PackInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RupyCost") + .HasColumnType("integer"); + + b.Property("SalesPeriodInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScheduleId") + .HasColumnType("integer"); + + b.Property("TicketCost") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SealedSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Attack") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Defense") + .HasColumnType("integer"); + + b.Property("IsFoil") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrimaryResourceCost") + .HasColumnType("integer"); + + b.Property("Rarity") + .HasColumnType("integer"); + + b.Property("ShadowverseCardSetEntryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("ShadowverseCardSetEntryId"); + + b.ToTable("Cards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBasic") + .HasColumnType("boolean"); + + b.Property("IsInRotation") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CardSets"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Format") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("MyRotationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("RandomLeaderSkin") + .HasColumnType("boolean"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("LeaderSkinId"); + + b.HasIndex("SleeveId"); + + b.HasIndex("ViewerId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Sleeves"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("NameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceCrystal") + .HasColumnType("integer"); + + b.Property("PriceRupy") + .HasColumnType("integer"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SleeveShopProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("SleeveShopSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpecialDeckFormatEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("SpecialDeckFormats"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpotCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("Cost") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("SpotCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpotCardExchangeEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ExchangePoint") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("TsRotationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SpotCardExchangeCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("RestrictionValue") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UnlimitedRestrictions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortUdid") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValueSql("nextval('\"ShortUdidSequence\"')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("ShortUdid"), "ShortUdidSequence"); + + b.Property("Udid") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShortUdid"); + + b.HasIndex("Udid") + .IsUnique(); + + b.ToTable("Viewers"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("AchievementType") + .HasColumnType("integer"); + + b.Property("AchievementStatus") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("NowAchievedLevel") + .HasColumnType("integer"); + + b.Property("ResultAnnounceSawLevel") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "AchievementType"); + + b.ToTable("ViewerAchievements"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassClaimEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Track") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "SeasonId"); + + b.HasIndex("ViewerId", "SeasonId", "Track", "Level") + .IsUnique(); + + b.ToTable("ViewerBattlePassClaims"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassProgressEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPremium") + .HasColumnType("boolean"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("WeeklyPeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("WeeklyPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "SeasonId") + .IsUnique(); + + b.ToTable("ViewerBattlePassProgress"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("EventKey") + .HasColumnType("text"); + + b.Property("Period") + .HasColumnType("text"); + + b.Property("Count") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "EventKey", "Period"); + + b.HasIndex("ViewerId", "Period"); + + b.ToTable("ViewerEventCounters"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerLeaderSkinSetClaim", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "SeriesId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerLeaderSkinSetClaims"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAt") + .HasColumnType("bigint"); + + b.Property("ClaimedAt") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MissionCatalogId") + .HasColumnType("integer"); + + b.Property("MissionStatus") + .HasColumnType("integer"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId"); + + b.HasIndex("ViewerId", "Slot") + .IsUnique(); + + b.ToTable("ViewerMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPuzzleClear", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("PuzzleId") + .HasColumnType("integer"); + + b.Property("BestRetryCount") + .HasColumnType("integer"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "PuzzleId"); + + b.ToTable("ViewerPuzzleClears"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerSpotCardExchange", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ExchangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.HasKey("ViewerId", "CardId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerSpotCardExchanges"); + }); + + modelBuilder.Entity("SleeveEntryViewer", b => + { + b.Property("SleevesId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("SleevesId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("SleeveEntryViewer"); + }); + + modelBuilder.Entity("DegreeEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.DegreeEntry", null) + .WithMany() + .HasForeignKey("DegreesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EmblemEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.EmblemEntry", null) + .WithMany() + .HasForeignKey("EmblemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LeaderSkinEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.LeaderSkinEntry", null) + .WithMany() + .HasForeignKey("LeaderSkinsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MyPageBackgroundEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.MyPageBackgroundEntry", null) + .WithMany() + .HasForeignKey("MyPageBackgroundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b => + { + b.HasOne("SVSim.Database.Entities.Story.StorySection", "Section") + .WithMany() + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Entities.Story.SpecialBattleSetting", "SpecialBattleSetting") + .WithMany() + .HasForeignKey("SpecialBattleSettingId"); + + b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterBattleSetting", "BattleSettings", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Battle3dFieldIdOverride") + .HasColumnType("integer"); + + b1.Property("BgmIdOverride") + .HasColumnType("integer"); + + b1.Property("DeckClassId") + .HasColumnType("integer"); + + b1.Property("DeckSkinIdOverride") + .HasColumnType("integer"); + + b1.Property("EnemyEmotionOverride") + .HasColumnType("integer"); + + b1.Property("PlayerEmotionOverride") + .HasColumnType("integer"); + + b1.Property("SkinIdOverride") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StoryChapterBattleSetting"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterReward", "Rewards", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StoryChapterReward"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.OwnsMany("SVSim.Database.Entities.Story.StorySubChapter", "SubChapters", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("IsMaintenanceChapter") + .HasColumnType("boolean"); + + b1.Property("SubChapterId") + .HasColumnType("integer"); + + b1.Property("SubChapterStoryId") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StorySubChapter"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.Navigation("BattleSettings"); + + b.Navigation("Rewards"); + + b.Navigation("Section"); + + b.Navigation("SpecialBattleSetting"); + + b.Navigation("SubChapters"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b => + { + b.HasOne("SVSim.Database.Entities.Story.StoryWorld", "World") + .WithMany() + .HasForeignKey("WorldId"); + + b.Navigation("World"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b => + { + b.HasOne("SVSim.Database.Models.BattlePassSeasonEntry", "Season") + .WithMany("Rewards") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckProductEntry", b => + { + b.HasOne("SVSim.Database.Models.BuildDeckSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.BuildDeckProductCardEntry", "Cards", b1 => + { + b1.Property("BuildDeckProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("IsSpot") + .HasColumnType("boolean"); + + b1.Property("Number") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckProductEntryId", "Id"); + + b1.ToTable("BuildDeckProductCardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckProductEntryId"); + }); + + b.OwnsMany("SVSim.Database.Models.BuildDeckProductRewardEntry", "Rewards", b1 => + { + b1.Property("BuildDeckProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("MessageId") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardIndex") + .HasColumnType("integer"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckProductEntryId", "Id"); + + b1.ToTable("BuildDeckProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckProductEntryId"); + }); + + b.Navigation("Cards"); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.OwnsMany("SVSim.Database.Models.BuildDeckSeriesRewardEntry", "SeriesRewards", b1 => + { + b1.Property("BuildDeckSeriesEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ItemIndex") + .HasColumnType("integer"); + + b1.Property("MessageId") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.Property("TierIndex") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckSeriesEntryId", "Id"); + + b1.ToTable("BuildDeckSeriesRewardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckSeriesEntryId"); + }); + + b.Navigation("SeriesRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b => + { + b.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Card"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany("LeaderSkins") + .HasForeignKey("ClassId"); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b => + { + b.HasOne("SVSim.Database.Models.LeaderSkinShopSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.LeaderSkinShopProductRewardEntry", "Rewards", b1 => + { + b1.Property("LeaderSkinShopProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("LeaderSkinShopProductEntryId", "Id"); + + b1.ToTable("LeaderSkinShopProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("LeaderSkinShopProductEntryId"); + }); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.OwnsMany("SVSim.Database.Models.LeaderSkinShopSeriesRewardEntry", "SetCompletionRewards", b1 => + { + b1.Property("LeaderSkinShopSeriesEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("LeaderSkinShopSeriesEntryId", "Id"); + + b1.ToTable("LeaderSkinShopSeriesRewardEntry"); + + b1.WithOwner() + .HasForeignKey("LeaderSkinShopSeriesEntryId"); + }); + + b.Navigation("SetCompletionRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b => + { + b.OwnsMany("SVSim.Database.Models.PackBannerEntry", "Banners", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("BannerName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("DialogTitle") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("PackConfigEntryId", "Id"); + + b1.ToTable("PackBannerEntry"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.OwnsMany("SVSim.Database.Models.PackChildGachaEntry", "ChildGachas", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CampaignName") + .HasColumnType("text"); + + b1.Property("CardCount") + .HasColumnType("integer"); + + b1.Property("Cost") + .HasColumnType("integer"); + + b1.Property("FreeGachaCampaignId") + .HasColumnType("integer"); + + b1.Property("GachaId") + .HasColumnType("integer"); + + b1.Property("IsDailySingle") + .HasColumnType("boolean"); + + b1.Property("ItemId") + .HasColumnType("bigint"); + + b1.Property("OverrideIncreaseGachaPoint") + .HasColumnType("integer"); + + b1.Property("PurchaseLimitCount") + .HasColumnType("integer"); + + b1.Property("TypeDetail") + .HasColumnType("integer"); + + b1.HasKey("PackConfigEntryId", "Id"); + + b1.ToTable("PackChildGachaEntry"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.OwnsOne("SVSim.Database.Models.PackGachaPointConfig", "GachaPointConfig", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("ExchangeablePoint") + .HasColumnType("integer"); + + b1.Property("IncreaseGachaPoint") + .HasColumnType("integer"); + + b1.HasKey("PackConfigEntryId"); + + b1.ToTable("Packs"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.Navigation("Banners"); + + b.Navigation("ChildGachas"); + + b.Navigation("GachaPointConfig"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleEntry", b => + { + b.HasOne("SVSim.Database.Models.PuzzleGroupEntry", "Group") + .WithMany("Puzzles") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId"); + + b.HasOne("SVSim.Database.Models.ShadowverseCardSetEntry", null) + .WithMany("Cards") + .HasForeignKey("ShadowverseCardSetEntryId"); + + b.OwnsOne("SVSim.Database.Models.CardCollectionInfo", "CollectionInfo", b1 => + { + b1.Property("ShadowverseCardEntryId") + .HasColumnType("bigint"); + + b1.Property("CraftCost") + .HasColumnType("integer"); + + b1.Property("DustReward") + .HasColumnType("integer"); + + b1.HasKey("ShadowverseCardEntryId"); + + b1.ToTable("Cards"); + + b1.WithOwner() + .HasForeignKey("ShadowverseCardEntryId"); + }); + + b.Navigation("Class"); + + b.Navigation("CollectionInfo"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin") + .WithMany() + .HasForeignKey("LeaderSkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.SleeveEntry", "Sleeve") + .WithMany() + .HasForeignKey("SleeveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Decks") + .HasForeignKey("ViewerId"); + + b.OwnsMany("SVSim.Database.Models.DeckCard", "Cards", b1 => + { + b1.Property("ShadowverseDeckEntryId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.HasKey("ShadowverseDeckEntryId", "Id"); + + b1.HasIndex("CardId"); + + b1.ToTable("DeckCard"); + + b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ShadowverseDeckEntryId"); + + b1.Navigation("Card"); + }); + + b.Navigation("Cards"); + + b.Navigation("Class"); + + b.Navigation("LeaderSkin"); + + b.Navigation("Sleeve"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b => + { + b.HasOne("SVSim.Database.Models.SleeveShopSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.SleeveShopProductRewardEntry", "Rewards", b1 => + { + b1.Property("SleeveShopProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("SleeveShopProductEntryId", "Id"); + + b1.ToTable("SleeveShopProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("SleeveShopProductEntryId"); + }); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.Property("IsProtected") + .HasColumnType("boolean"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("CardId"); + + b1.HasIndex("ViewerId", "CardId") + .IsUnique(); + + b1.ToTable("OwnedCardEntry"); + + b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + + b1.Navigation("Card"); + }); + + b.OwnsMany("SVSim.Database.Models.OwnedItemEntry", "Items", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.Property("ItemId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ItemId"); + + b1.HasIndex("ViewerId", "ItemId") + .IsUnique(); + + b1.ToTable("OwnedItemEntry"); + + b1.HasOne("SVSim.Database.Models.ItemEntry", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Item"); + + b1.Navigation("Viewer"); + }); + + b.OwnsMany("SVSim.Database.Models.SocialAccountConnection", "SocialAccountConnections", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AccountId") + .HasColumnType("numeric(20,0)"); + + b1.Property("AccountType") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("AccountType", "AccountId") + .IsUnique(); + + b1.ToTable("SocialAccountConnection"); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Viewer"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerBuildDeckProductPurchase", "BuildDeckPurchases", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ProductId") + .HasColumnType("integer"); + + b1.Property("PurchaseCount") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "ProductId") + .IsUnique(); + + b1.ToTable("ViewerBuildDeckProductPurchase"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerClassData", "Classes", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ClassId") + .HasColumnType("integer"); + + b1.Property("Exp") + .HasColumnType("integer"); + + b1.Property("LeaderSkinId") + .HasColumnType("integer"); + + b1.Property("Level") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ClassId"); + + b1.HasIndex("LeaderSkinId"); + + b1.ToTable("ViewerClassData"); + + b1.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin") + .WithMany() + .HasForeignKey("LeaderSkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Class"); + + b1.Navigation("LeaderSkin"); + + b1.Navigation("Viewer"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerCurrency", "Currency", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("AndroidCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("Crystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("DmmCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("FreeCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("IosCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("LifeTotalCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("RedEther") + .HasColumnType("numeric(20,0)"); + + b1.Property("Rupees") + .HasColumnType("numeric(20,0)"); + + b1.Property("SpotPoints") + .HasColumnType("numeric(20,0)"); + + b1.Property("SteamCrystals") + .HasColumnType("numeric(20,0)"); + + b1.HasKey("ViewerId"); + + b1.ToTable("Viewers"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerInfo", "Info", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b1.Property("CountryCode") + .IsRequired() + .HasColumnType("text"); + + b1.Property("IsOfficial") + .HasColumnType("boolean"); + + b1.Property("IsOfficialMarkDisplayed") + .HasColumnType("boolean"); + + b1.Property("MaxFriends") + .HasColumnType("integer"); + + b1.Property("SelectedDegreeId") + .HasColumnType("integer"); + + b1.Property("SelectedEmblemId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId"); + + b1.HasIndex("SelectedDegreeId"); + + b1.HasIndex("SelectedEmblemId"); + + b1.ToTable("Viewers"); + + b1.HasOne("SVSim.Database.Models.DegreeEntry", "SelectedDegree") + .WithMany() + .HasForeignKey("SelectedDegreeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.HasOne("SVSim.Database.Models.EmblemEntry", "SelectedEmblem") + .WithMany() + .HasForeignKey("SelectedEmblemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + + b1.Navigation("SelectedDegree"); + + b1.Navigation("SelectedEmblem"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerMissionData", "MissionData", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("HasReceivedPickTwoMission") + .HasColumnType("boolean"); + + b1.Property("MissionChangeTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("MissionReceiveType") + .HasColumnType("integer"); + + b1.Property("TutorialState") + .HasColumnType("integer"); + + b1.HasKey("ViewerId"); + + b1.ToTable("Viewers"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerPackOpenCount", "PackOpenCounts", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("LastDailyFreeAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("OpenCount") + .HasColumnType("integer"); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.ToTable("ViewerPackOpenCount"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.Navigation("BuildDeckPurchases"); + + b.Navigation("Cards"); + + b.Navigation("Classes"); + + b.Navigation("Currency") + .IsRequired(); + + b.Navigation("Info") + .IsRequired(); + + b.Navigation("Items"); + + b.Navigation("MissionData") + .IsRequired(); + + b.Navigation("PackOpenCounts"); + + b.Navigation("SocialAccountConnections"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Achievements") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("EventCounters") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Missions") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SleeveEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.SleeveEntry", null) + .WithMany() + .HasForeignKey("SleevesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassSeasonEntry", b => + { + b.Navigation("Rewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b => + { + b.Navigation("LeaderSkins"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b => + { + b.Navigation("Puzzles"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Navigation("Achievements"); + + b.Navigation("Decks"); + + b.Navigation("EventCounters"); + + b.Navigation("Missions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.cs b/SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.cs new file mode 100644 index 0000000..4a62b1e --- /dev/null +++ b/SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + /// + public partial class AddSpotCardExchange : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Currency_SpotPoints", + table: "Viewers", + type: "numeric(20,0)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.CreateTable( + name: "SpotCardExchangeCatalog", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + CardId = table.Column(type: "bigint", nullable: false), + ClassId = table.Column(type: "integer", nullable: false), + ExchangePoint = table.Column(type: "integer", nullable: false), + TsRotationId = table.Column(type: "bigint", nullable: false), + IsPreRelease = table.Column(type: "boolean", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SpotCardExchangeCatalog", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ViewerSpotCardExchanges", + columns: table => new + { + ViewerId = table.Column(type: "bigint", nullable: false), + CardId = table.Column(type: "bigint", nullable: false), + IsPreRelease = table.Column(type: "boolean", nullable: false), + ExchangedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ViewerSpotCardExchanges", x => new { x.ViewerId, x.CardId }); + }); + + migrationBuilder.CreateIndex( + name: "IX_ViewerSpotCardExchanges_ViewerId", + table: "ViewerSpotCardExchanges", + column: "ViewerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SpotCardExchangeCatalog"); + + migrationBuilder.DropTable( + name: "ViewerSpotCardExchanges"); + + migrationBuilder.DropColumn( + name: "Currency_SpotPoints", + table: "Viewers"); + } + } +} diff --git a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs index 558cfdb..4b747c9 100644 --- a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs +++ b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs @@ -2206,6 +2206,40 @@ namespace SVSim.Database.Migrations b.ToTable("SpotCards"); }); + modelBuilder.Entity("SVSim.Database.Models.SpotCardExchangeEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ExchangePoint") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("TsRotationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SpotCardExchangeCatalog"); + }); + modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b => { b.Property("Id") @@ -2473,6 +2507,27 @@ namespace SVSim.Database.Migrations b.ToTable("ViewerPuzzleClears"); }); + modelBuilder.Entity("SVSim.Database.Models.ViewerSpotCardExchange", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ExchangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.HasKey("ViewerId", "CardId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerSpotCardExchanges"); + }); + modelBuilder.Entity("SleeveEntryViewer", b => { b.Property("SleevesId") @@ -3390,6 +3445,9 @@ namespace SVSim.Database.Migrations b1.Property("Rupees") .HasColumnType("numeric(20,0)"); + b1.Property("SpotPoints") + .HasColumnType("numeric(20,0)"); + b1.Property("SteamCrystals") .HasColumnType("numeric(20,0)"); diff --git a/SVSim.Database/Models/SpotCardExchangeEntry.cs b/SVSim.Database/Models/SpotCardExchangeEntry.cs new file mode 100644 index 0000000..fa83eef --- /dev/null +++ b/SVSim.Database/Models/SpotCardExchangeEntry.cs @@ -0,0 +1,30 @@ +using SVSim.Database.Common; + +namespace SVSim.Database.Models; + +/// +/// One catalog entry of the /spot_card_exchange/top shop — a card the viewer can buy with +/// spot points. PK = wire card_id. Distinct from (which is the +/// /load/index data.spot_cards rental-cost list — a different concept). +/// +/// matches the card_set_id; cards cycle out of the exchange when +/// their set rotates. distinguishes the pre-release-pool subset +/// gated by pre_release_spot_card_exchange_limit. +/// +/// +public class SpotCardExchangeEntry : BaseEntity +{ + public long CardId { get => Id; set => Id = value; } + + /// Wire class field — clan id (0=Neutral, 1=Forestcraft, ..., 8). + public int ClassId { get; set; } + + public int ExchangePoint { get; set; } + + /// Wire ts_rotation_id — card_set_id this card belongs to. + public long TsRotationId { get; set; } + + public bool IsPreRelease { get; set; } + + public bool IsEnabled { get; set; } +} diff --git a/SVSim.Database/Models/ViewerCurrency.cs b/SVSim.Database/Models/ViewerCurrency.cs index fbd5052..9972c53 100644 --- a/SVSim.Database/Models/ViewerCurrency.cs +++ b/SVSim.Database/Models/ViewerCurrency.cs @@ -14,4 +14,11 @@ public class ViewerCurrency public ulong LifeTotalCrystals { get; set; } public ulong RedEther { get; set; } public ulong Rupees { get; set; } + + /// + /// Spot card points — currency earned from battles/missions, spent at /spot_card_exchange/exchange. + /// Wire field spot_point in /load/index and /spot_card_exchange/top; reward_type 12 + /// () in reward_list entries. + /// + public ulong SpotPoints { get; set; } } \ No newline at end of file diff --git a/SVSim.Database/Models/ViewerSpotCardExchange.cs b/SVSim.Database/Models/ViewerSpotCardExchange.cs new file mode 100644 index 0000000..b3c2b5e --- /dev/null +++ b/SVSim.Database/Models/ViewerSpotCardExchange.cs @@ -0,0 +1,16 @@ +namespace SVSim.Database.Models; + +/// +/// One row per (viewer, exchanged card). Composite PK (ViewerId, CardId). Standalone table +/// (not a Viewer owned collection) to avoid cartesian-explode on viewer-graph reads. +/// snapshot at exchange time so the pre-release counter can be +/// computed without joining back to (and to survive +/// catalog edits that re-classify a card). +/// +public class ViewerSpotCardExchange +{ + public long ViewerId { get; set; } + public long CardId { get; set; } + public bool IsPreRelease { get; set; } + public DateTime ExchangedAt { get; set; } +} diff --git a/SVSim.Database/SVSimDbContext.cs b/SVSim.Database/SVSimDbContext.cs index 43efff2..1a503d1 100644 --- a/SVSim.Database/SVSimDbContext.cs +++ b/SVSim.Database/SVSimDbContext.cs @@ -76,6 +76,8 @@ public class SVSimDbContext : DbContext public DbSet LeaderSkinShopSeries => Set(); public DbSet LeaderSkinShopProducts => Set(); public DbSet ViewerLeaderSkinSetClaims => Set(); + public DbSet SpotCardExchangeCatalog => Set(); + public DbSet ViewerSpotCardExchanges => Set(); public DbSet MaintenanceCards => Set(); public DbSet FeatureMaintenances => Set(); public DbSet PreReleaseInfos => Set(); @@ -209,6 +211,12 @@ public class SVSimDbContext : DbContext b.HasIndex(c => c.ViewerId); }); + modelBuilder.Entity(b => + { + b.HasKey(e => new { e.ViewerId, e.CardId }); + b.HasIndex(e => e.ViewerId); + }); + modelBuilder.Entity(b => { b.HasKey(r => new { r.CardId, r.Type, r.CosmeticId }); diff --git a/SVSim.Database/Services/RewardGrantService.cs b/SVSim.Database/Services/RewardGrantService.cs index e7c5a0f..4d4c6d5 100644 --- a/SVSim.Database/Services/RewardGrantService.cs +++ b/SVSim.Database/Services/RewardGrantService.cs @@ -20,8 +20,9 @@ public sealed record GrantedReward(int RewardType, long RewardId, int RewardNum) /// /// /// DO NOT reimplement reward dispatch in a controller or new helper. This service handles -/// RedEther, Crystal, Item, Card (with cascade), Sleeve, Emblem, -/// Degree, Rupy, Skin, MyPageBG — everything except SpotCard (TODO). Endpoint code that takes a +/// RedEther, Crystal, SpotCardPoint, Item, Card (with cascade), +/// Sleeve, Emblem, Degree, Rupy, Skin, MyPageBG — everything except the dead-letter SpotCard / +/// SpotCardOnlyLatestCardPack slots (use Card=5 instead). Endpoint code that takes a /// list of (type, id, num) tuples should iterate and call /// per tuple — never switch on type yourself, never filter to "only card-typed rewards", never /// build a second dispatch table. Past duplicate implementations (ICardAcquisitionService in the @@ -87,6 +88,10 @@ public sealed class RewardGrantService viewer.Currency.RedEther += (ulong)num; return Single(type, detailId, checked((int)viewer.Currency.RedEther)); + case UserGoodsType.SpotCardPoint: + viewer.Currency.SpotPoints += (ulong)num; + return Single(type, detailId, checked((int)viewer.Currency.SpotPoints)); + case UserGoodsType.Item: { var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId); @@ -106,11 +111,11 @@ public sealed class RewardGrantService case UserGoodsType.SpotCard: case UserGoodsType.SpotCardOnlyLatestCardPack: - // TODO: spot cards are currently global in our seed data; the existence of these - // reward types suggests there's a mix of global + per-player spot cards. Revisit - // when per-player spot-card infrastructure lands. + // Spot-card-typed grants don't appear in captures — emitters always use Card=5 + // with the spot-card-specific id. These two enum slots remain unimplemented; if a + // capture ever shows one in a reward_list we'll know to wire them up here. throw new NotSupportedException( - $"{type} rewards are not yet supported — see SpotCard TODO in RewardGrantService."); + $"{type} rewards are not yet supported — emitters use Card=5 instead."); default: throw new NotSupportedException($"UserGoodsType {type} not yet handled by RewardGrantService"); diff --git a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs index d0f6f6b..7b10a6f 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs @@ -181,6 +181,7 @@ public class LoadController : SVSimController UserInfo = new UserInfo(deviceType, viewer), UserCurrency = new UserCurrency(viewer), UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(), + SpotPoint = checked((int)viewer.Currency.SpotPoints), UserRotationDecks = new UserFormatDeckInfo { UserDecks = viewer.Decks.Where(d => d.Format == Format.Rotation) diff --git a/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs b/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs new file mode 100644 index 0000000..ff85f78 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs @@ -0,0 +1,192 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Services; +using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.SpotCardExchange; +using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.SpotCardExchange; + +namespace SVSim.EmulatedEntrypoint.Controllers; + +/// +/// /spot_card_exchange/* — trade spot points for individual cards from the rotating exchange +/// pool. Spot points are earned from battles/missions (not implemented here — earners live in +/// battle/mission finish reward emitters via + +/// ). +/// +[Route("spot_card_exchange")] +public class SpotCardExchangeController : SVSimController +{ + /// + /// Pre-release exchange cap. Captures show "2" — global limit, not per-card. When + /// IsPreRelease is active on the catalog level we honour this; otherwise the cap is + /// effectively unbounded (UI never shows the warning). + /// + private const int PreReleaseLimit = 2; + + private readonly SVSimDbContext _db; + private readonly RewardGrantService _rewards; + private readonly TimeProvider _time; + + public SpotCardExchangeController(SVSimDbContext db, RewardGrantService rewards, TimeProvider time) + { + _db = db; + _rewards = rewards; + _time = time; + } + + [HttpPost("top")] + public async Task> Top() + { + if (!TryGetViewerId(out long viewerId)) return Unauthorized(); + + var viewer = await _db.Viewers + .Where(v => v.Id == viewerId) + .Select(v => new { v.Currency.SpotPoints }) + .FirstOrDefaultAsync(); + if (viewer is null) return Unauthorized(); + + var catalog = await _db.SpotCardExchangeCatalog + .Where(c => c.IsEnabled) + .OrderBy(c => c.Id) + .ToListAsync(); + + var exchanges = await _db.ViewerSpotCardExchanges + .Where(e => e.ViewerId == viewerId) + .ToListAsync(); + var exchangedIds = exchanges.Select(e => e.CardId).ToHashSet(); + int preReleaseExchangedCount = exchanges.Count(e => e.IsPreRelease); + bool preReleaseActive = catalog.Any(c => c.IsPreRelease); + bool preReleaseLimitHit = preReleaseExchangedCount >= PreReleaseLimit; + + // Build the 9-clan-bucket dict-of-arrays. Every clan slot is present even when empty; + // the inner dict always uses key "1" matching the captured prod shape. + var byClan = new List>>(9); + for (int clan = 0; clan < 9; clan++) + { + byClan.Add(new Dictionary> + { + ["1"] = new List(), + }); + } + + foreach (var c in catalog) + { + int clanIdx = Math.Clamp(c.ClassId, 0, 8); + byClan[clanIdx]["1"].Add(new SpotCardExchangeCardDto + { + CardId = c.Id, + ExchangeStatus = ComputeExchangeStatus(c, exchangedIds, preReleaseLimitHit), + ExchangePoint = c.ExchangePoint.ToString(), + Class = c.ClassId.ToString(), + IsPreRelease = c.IsPreRelease, + TsRotationId = c.TsRotationId.ToString(), + }); + } + + return new SpotCardExchangeTopResponse + { + SpotPoint = checked((int)viewer.SpotPoints), + ExchangeableCardList = byClan, + SoonCycleOutCardSetId = string.Empty, // No captured value to derive; spec allows "" + PreReleaseInfo = new PreReleaseInfoDto + { + IsPreRelease = preReleaseActive, + PreReleaseSpotCardExchangeCount = preReleaseExchangedCount, + PreReleaseSpotCardExchangeLimit = PreReleaseLimit, + }, + }; + } + + [HttpPost("exchange")] + public async Task> Exchange(SpotCardExchangeRequest request) + { + if (!TryGetViewerId(out long viewerId)) return Unauthorized(); + + var entry = await _db.SpotCardExchangeCatalog.FindAsync((long)request.CardId); + if (entry is null || !entry.IsEnabled) + return BadRequest(new { error = "unknown_card" }); + + // Already-exchanged guard — each catalog row is one card per viewer. + var existingExchange = await _db.ViewerSpotCardExchanges + .FirstOrDefaultAsync(e => e.ViewerId == viewerId && e.CardId == entry.Id); + if (existingExchange is not null) + return BadRequest(new { error = "already_exchanged" }); + + if (entry.IsPreRelease) + { + int prCount = await _db.ViewerSpotCardExchanges + .CountAsync(e => e.ViewerId == viewerId && e.IsPreRelease); + if (prCount >= PreReleaseLimit) + return BadRequest(new { error = "pre_release_limit_reached" }); + } + + var viewer = await LoadViewerGraphAsync(viewerId); + + var rewardList = new List(); + + // Debit spot points. Client-supplied exchange_point isn't authoritative — server uses + // catalog price. Mirroring the build_deck/sleeve convention: post-state currency entry + // first, then grants. + if (viewer.Currency.SpotPoints < (ulong)entry.ExchangePoint) + return BadRequest(new { error = "insufficient_spot_points" }); + viewer.Currency.SpotPoints -= (ulong)entry.ExchangePoint; + rewardList.Add(new RewardListEntry + { + RewardType = (int)UserGoodsType.SpotCardPoint, + RewardId = 0, + RewardNum = checked((int)viewer.Currency.SpotPoints), + }); + + // Grant the card itself via the existing card dispatcher (handles cosmetic cascade). + var granted = await _rewards.ApplyAsync(viewer, UserGoodsType.Card, entry.Id, 1); + foreach (var g in granted) + { + rewardList.Add(new RewardListEntry + { + RewardType = g.RewardType, + RewardId = g.RewardId, + RewardNum = g.RewardNum, + }); + } + + _db.ViewerSpotCardExchanges.Add(new ViewerSpotCardExchange + { + ViewerId = viewerId, + CardId = entry.Id, + IsPreRelease = entry.IsPreRelease, + ExchangedAt = _time.GetUtcNow().UtcDateTime, + }); + + await _db.SaveChangesAsync(); + return new SpotCardExchangeResponse { RewardList = rewardList }; + } + + /// + /// Maps to : + /// 0 = EnableExchange + /// 1 = AlreadyExchange (viewer has already exchanged this card) + /// 2 = LimitOver (pre-release card and viewer hit the global pre-release cap) + /// Insufficient-balance is NOT surfaced here — the client greys those out by comparing + /// spot_point to exchange_point. + /// + private static int ComputeExchangeStatus(SpotCardExchangeEntry c, HashSet exchangedIds, bool preReleaseLimitHit) + { + if (exchangedIds.Contains(c.Id)) return 1; + if (c.IsPreRelease && preReleaseLimitHit) return 2; + return 0; + } + + private Task LoadViewerGraphAsync(long viewerId) => _db.Viewers + .Include(v => v.Cards).ThenInclude(c => c.Card) + .Include(v => v.Sleeves) + .Include(v => v.Emblems) + .Include(v => v.LeaderSkins) + .Include(v => v.Degrees) + .Include(v => v.MyPageBackgrounds) + .Include(v => v.Items).ThenInclude(i => i.Item) + .AsSplitQuery() + .FirstAsync(v => v.Id == viewerId); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/SpotCardExchange/SpotCardExchangeRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/SpotCardExchange/SpotCardExchangeRequest.cs new file mode 100644 index 0000000..63a7944 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/SpotCardExchange/SpotCardExchangeRequest.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.SpotCardExchange; + +/// +/// /spot_card_exchange/exchange request — trade spot points for +/// the card identified by . The exchange_point field is the client's view +/// of the price (sanity-check it against the catalog server-side). +/// +[MessagePackObject] +public class SpotCardExchangeRequest : BaseRequest +{ + [JsonPropertyName("card_id")] + [Key("card_id")] + public int CardId { get; set; } + + [JsonPropertyName("exchange_point")] + [Key("exchange_point")] + public int ExchangePoint { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeResponse.cs new file mode 100644 index 0000000..d0ad0d9 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeResponse.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.SpotCardExchange; + +/// +/// /spot_card_exchange/exchange response. reward_list entries follow the standard +/// shape: SpotCardPoint debit post-state first, then the card grant (with cosmetic cascade +/// if applicable). +/// +[MessagePackObject] +public class SpotCardExchangeResponse +{ + [JsonPropertyName("reward_list")] + [Key("reward_list")] + public List RewardList { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeTopResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeTopResponse.cs new file mode 100644 index 0000000..4983ca5 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/SpotCardExchange/SpotCardExchangeTopResponse.cs @@ -0,0 +1,88 @@ +using System.Text.Json.Serialization; +using MessagePack; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.SpotCardExchange; + +/// +/// /spot_card_exchange/top response. +/// +/// exchangeable_card_list is an array of exactly 9 entries indexed by clan id 0..8. +/// Each entry is a dict keyed by an arbitrary stringified int (prod always emits "1") whose +/// value is the array of cards for that clan. The client iterates by clan index then dict-keys +/// (LitJson positional iteration). +/// +/// +/// pre_relase_info — WIRE TYPO PRESERVED ("relase" not "release"). Renaming this field +/// breaks the client's jsonData["pre_relase_info"] access. +/// +/// +[MessagePackObject] +public class SpotCardExchangeTopResponse +{ + [JsonPropertyName("spot_point")] + [Key("spot_point")] + public int SpotPoint { get; set; } + + [JsonPropertyName("exchangeable_card_list")] + [Key("exchangeable_card_list")] + public List>> ExchangeableCardList { get; set; } = new(); + + /// Card set id about to cycle out of spot-card eligibility — drives "last chance!" UI. + /// Empty string in the captured response. Stays string-typed because the client uses + /// int.TryParse. + [JsonPropertyName("soon_cycle_out_card_set_id")] + [Key("soon_cycle_out_card_set_id")] + public string SoonCycleOutCardSetId { get; set; } = string.Empty; + + [JsonPropertyName("pre_relase_info")] + [Key("pre_relase_info")] + public PreReleaseInfoDto PreReleaseInfo { get; set; } = new(); +} + +[MessagePackObject] +public class SpotCardExchangeCardDto +{ + [JsonPropertyName("card_id")] + [Key("card_id")] + public long CardId { get; set; } + + /// SpotCardExchangeInfo.ExchangeStatus — 0=EnableExchange, 1=AlreadyExchange, 2=LimitOver. + [JsonPropertyName("exchange_status")] + [Key("exchange_status")] + public int ExchangeStatus { get; set; } + + /// Stringified price — prod ships e.g. "3500", client reads via .ToInt(). + [JsonPropertyName("exchange_point")] + [Key("exchange_point")] + public string ExchangePoint { get; set; } = "0"; + + /// Stringified clan id. Prod ships "0".."8". + [JsonPropertyName("class")] + [Key("class")] + public string Class { get; set; } = "0"; + + [JsonPropertyName("is_pre_release")] + [Key("is_pre_release")] + public bool IsPreRelease { get; set; } + + /// Stringified card_set_id this card belongs to. + [JsonPropertyName("ts_rotation_id")] + [Key("ts_rotation_id")] + public string TsRotationId { get; set; } = "0"; +} + +[MessagePackObject] +public class PreReleaseInfoDto +{ + [JsonPropertyName("is_pre_release")] + [Key("is_pre_release")] + public bool IsPreRelease { get; set; } + + [JsonPropertyName("pre_release_spot_card_exchange_count")] + [Key("pre_release_spot_card_exchange_count")] + public int PreReleaseSpotCardExchangeCount { get; set; } + + [JsonPropertyName("pre_release_spot_card_exchange_limit")] + [Key("pre_release_spot_card_exchange_limit")] + public int PreReleaseSpotCardExchangeLimit { get; set; } +} diff --git a/SVSim.UnitTests/Controllers/SpotCardExchangeControllerTests.cs b/SVSim.UnitTests/Controllers/SpotCardExchangeControllerTests.cs new file mode 100644 index 0000000..25f908b --- /dev/null +++ b/SVSim.UnitTests/Controllers/SpotCardExchangeControllerTests.cs @@ -0,0 +1,216 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Controllers; + +public class SpotCardExchangeControllerTests +{ + private static StringContent JsonBody(string json) => new(json, Encoding.UTF8, "application/json"); + + /// + /// Seeds 3 catalog rows: a regular class-0 card, a regular class-1 card, and a pre-release + /// card. Plus card-catalog rows so RewardGrantService can resolve the grant. Caller sets + /// viewer SpotPoints. + /// + private static async Task SeedCatalog(SVSimTestFactory f) + { + using var scope = f.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + if (!await db.Cards.AnyAsync(c => c.Id == 900100001L)) + db.Cards.Add(new ShadowverseCardEntry { Id = 900100001L, Name = "TestSpotNeutral", Rarity = Rarity.Bronze }); + if (!await db.Cards.AnyAsync(c => c.Id == 900100002L)) + db.Cards.Add(new ShadowverseCardEntry { Id = 900100002L, Name = "TestSpotClan1", Rarity = Rarity.Bronze }); + if (!await db.Cards.AnyAsync(c => c.Id == 900100099L)) + db.Cards.Add(new ShadowverseCardEntry { Id = 900100099L, Name = "TestSpotPreRelease", Rarity = Rarity.Gold }); + + db.SpotCardExchangeCatalog.AddRange( + new SpotCardExchangeEntry { Id = 900100001L, ClassId = 0, ExchangePoint = 3500, TsRotationId = 10001, IsPreRelease = false, IsEnabled = true }, + new SpotCardExchangeEntry { Id = 900100002L, ClassId = 1, ExchangePoint = 3500, TsRotationId = 10001, IsPreRelease = false, IsEnabled = true }, + new SpotCardExchangeEntry { Id = 900100099L, ClassId = 0, ExchangePoint = 1000, TsRotationId = 10001, IsPreRelease = true, IsEnabled = true }); + await db.SaveChangesAsync(); + } + + private static async Task SetSpotPoints(SVSimTestFactory f, long viewerId, ulong points) + { + using var scope = f.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var v = await db.Viewers.FirstAsync(x => x.Id == viewerId); + v.Currency.SpotPoints = points; + await db.SaveChangesAsync(); + } + + [Test] + public async Task Top_returns_9_clan_buckets_with_pre_relase_info_typo() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await SeedCatalog(factory); + await SetSpotPoints(factory, viewerId, 5000); + + using var client = factory.CreateAuthenticatedClient(viewerId); + var response = await client.PostAsync("/spot_card_exchange/top", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""")); + + var body = await response.Content.ReadAsStringAsync(); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body); + + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + Assert.That(root.GetProperty("spot_point").GetInt32(), Is.EqualTo(5000)); + + var ecl = root.GetProperty("exchangeable_card_list"); + Assert.That(ecl.GetArrayLength(), Is.EqualTo(9), "wire shape: array of exactly 9 clan buckets"); + + // Clan 0 bucket should have 2 cards (class-0 neutral + pre-release in our seed). + var clan0 = ecl[0].GetProperty("1"); + Assert.That(clan0.GetArrayLength(), Is.EqualTo(2)); + + // Wire typo preserved + Assert.That(root.TryGetProperty("pre_relase_info", out var prInfo), Is.True); + Assert.That(root.TryGetProperty("pre_release_info", out _), Is.False, "the typo-free spelling must NOT be emitted"); + Assert.That(prInfo.GetProperty("pre_release_spot_card_exchange_limit").GetInt32(), Is.EqualTo(2)); + Assert.That(prInfo.GetProperty("is_pre_release").GetBoolean(), Is.True, "catalog has a pre-release card"); + } + + [Test] + public async Task Exchange_debits_spot_points_and_grants_card() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await SeedCatalog(factory); + await SetSpotPoints(factory, viewerId, 5000); + + using var client = factory.CreateAuthenticatedClient(viewerId); + var response = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":900100001,"exchange_point":3500}""")); + + var body = await response.Content.ReadAsStringAsync(); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body); + + using var doc = JsonDocument.Parse(body); + var rewardList = doc.RootElement.GetProperty("reward_list"); + Assert.That(rewardList.GetArrayLength(), Is.EqualTo(2)); // SpotCardPoint post-state + Card grant + + // Debit: SpotCardPoint type=12, id=0, post-state 1500 (5000 - 3500) + var debit = rewardList[0]; + Assert.That(debit.GetProperty("reward_type").GetInt32(), Is.EqualTo(12)); + Assert.That(debit.GetProperty("reward_id").GetInt64(), Is.EqualTo(0)); + Assert.That(debit.GetProperty("reward_num").GetInt32(), Is.EqualTo(1500)); + + // Grant: Card type=5, id=card id, count=1 + var grant = rewardList[1]; + Assert.That(grant.GetProperty("reward_type").GetInt32(), Is.EqualTo(5)); + Assert.That(grant.GetProperty("reward_id").GetInt64(), Is.EqualTo(900100001L)); + + // ViewerSpotCardExchange + viewer.Cards persisted + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var record = await db.ViewerSpotCardExchanges.FirstOrDefaultAsync(e => e.ViewerId == viewerId && e.CardId == 900100001L); + Assert.That(record, Is.Not.Null); + var owned = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card) + .FirstAsync(v => v.Id == viewerId); + Assert.That(owned.Cards.Any(c => c.Card.Id == 900100001L), Is.True); + Assert.That(owned.Currency.SpotPoints, Is.EqualTo(1500UL)); + } + + [Test] + public async Task Exchange_with_insufficient_points_returns_400() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await SeedCatalog(factory); + await SetSpotPoints(factory, viewerId, 100); + + using var client = factory.CreateAuthenticatedClient(viewerId); + var response = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":900100001,"exchange_point":3500}""")); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + + // No exchange row should have been created + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + Assert.That(await db.ViewerSpotCardExchanges.CountAsync(e => e.ViewerId == viewerId), Is.EqualTo(0)); + } + + [Test] + public async Task Exchange_already_exchanged_card_returns_400() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + await SeedCatalog(factory); + await SetSpotPoints(factory, viewerId, 7000); + + using var client = factory.CreateAuthenticatedClient(viewerId); + var first = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":900100001,"exchange_point":3500}""")); + Assert.That(first.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + var second = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":900100001,"exchange_point":3500}""")); + Assert.That(second.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + } + + [Test] + public async Task Pre_release_limit_blocks_third_exchange_and_top_reports_LimitOver_status() + { + // Seed 3 pre-release cards; viewer can exchange 2 then hits the limit on the 3rd. + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + for (int i = 0; i < 3; i++) + { + long cid = 800100001L + i; + if (!await db.Cards.AnyAsync(c => c.Id == cid)) + db.Cards.Add(new ShadowverseCardEntry { Id = cid, Name = $"PR{i}", Rarity = Rarity.Bronze }); + db.SpotCardExchangeCatalog.Add(new SpotCardExchangeEntry + { + Id = cid, ClassId = 0, ExchangePoint = 100, TsRotationId = 10099, IsPreRelease = true, IsEnabled = true, + }); + } + await db.SaveChangesAsync(); + } + await SetSpotPoints(factory, viewerId, 10000); + + using var client = factory.CreateAuthenticatedClient(viewerId); + // Two successful pre-release exchanges + var r1 = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":800100001,"exchange_point":100}""")); + Assert.That(r1.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + var r2 = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":800100002,"exchange_point":100}""")); + Assert.That(r2.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + // Third one rejected by pre-release limit (limit==2) + var r3 = await client.PostAsync("/spot_card_exchange/exchange", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","card_id":800100003,"exchange_point":100}""")); + Assert.That(r3.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + + // /top should report status=2 (LimitOver) for the remaining pre-release card + var top = await client.PostAsync("/spot_card_exchange/top", + JsonBody("""{"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""")); + var topBody = await top.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(topBody); + var clan0 = doc.RootElement.GetProperty("exchangeable_card_list")[0].GetProperty("1"); + int? statusFor800100003 = null; + foreach (var card in clan0.EnumerateArray()) + { + if (card.GetProperty("card_id").GetInt64() == 800100003L) + statusFor800100003 = card.GetProperty("exchange_status").GetInt32(); + } + Assert.That(statusFor800100003, Is.EqualTo(2), "unexchanged pre-release card after hitting limit should show LimitOver"); + + var prCount = doc.RootElement.GetProperty("pre_relase_info").GetProperty("pre_release_spot_card_exchange_count").GetInt32(); + Assert.That(prCount, Is.EqualTo(2)); + } +} diff --git a/SVSim.UnitTests/Importers/SpotCardExchangeImporterTests.cs b/SVSim.UnitTests/Importers/SpotCardExchangeImporterTests.cs new file mode 100644 index 0000000..bfd9aae --- /dev/null +++ b/SVSim.UnitTests/Importers/SpotCardExchangeImporterTests.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Bootstrap.Importers; +using SVSim.Database; +using SVSim.Database.Models; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Importers; + +public class SpotCardExchangeImporterTests +{ + private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds"); + + [Test] + public async Task Imports_catalog_from_seed_file() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new SpotCardExchangeImporter().ImportAsync(db, SeedDir); + + var entries = await db.SpotCardExchangeCatalog.ToListAsync(); + Assert.That(entries.Count, Is.GreaterThan(0)); + + // Spot-check: card 113041010 (class 0, exchange_point 3500, ts_rotation_id 10013) + var c = entries.FirstOrDefault(e => e.Id == 113041010); + Assert.That(c, Is.Not.Null); + Assert.That(c!.ClassId, Is.EqualTo(0)); + Assert.That(c.ExchangePoint, Is.EqualTo(3500)); + Assert.That(c.TsRotationId, Is.EqualTo(10013)); + Assert.That(c.IsPreRelease, Is.False); + } + + [Test] + public async Task Is_idempotent_on_rerun() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new SpotCardExchangeImporter().ImportAsync(db, SeedDir); + int before = await db.SpotCardExchangeCatalog.CountAsync(); + await new SpotCardExchangeImporter().ImportAsync(db, SeedDir); + int after = await db.SpotCardExchangeCatalog.CountAsync(); + + Assert.That(after, Is.EqualTo(before)); + } + + [Test] + public async Task Leaves_existing_rows_untouched_when_missing_from_seed() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + const long legacyId = 999_999_999L; + db.SpotCardExchangeCatalog.Add(new SpotCardExchangeEntry + { + Id = legacyId, ClassId = 9, ExchangePoint = 99999, TsRotationId = 1, IsEnabled = true, + }); + await db.SaveChangesAsync(); + + await new SpotCardExchangeImporter().ImportAsync(db, SeedDir); + + var legacy = await db.SpotCardExchangeCatalog.FindAsync(legacyId); + Assert.That(legacy, Is.Not.Null); + Assert.That(legacy!.ExchangePoint, Is.EqualTo(99999)); + } +} diff --git a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs index 405766c..08bddeb 100644 --- a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs +++ b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs @@ -212,6 +212,7 @@ internal sealed class SVSimTestFactory : WebApplicationFactory await new SleeveShopImporter().ImportAsync(ctx, seedDir); await new ItemPurchaseImporter().ImportAsync(ctx, seedDir); await new LeaderSkinShopImporter().ImportAsync(ctx, seedDir); + await new SpotCardExchangeImporter().ImportAsync(ctx, seedDir); var puzzleImporter = new PuzzleImporter(); await puzzleImporter.ImportGroupsAsync(ctx, seedDir); await puzzleImporter.ImportPuzzlesAsync(ctx, seedDir);