diff --git a/SodaLive/Sources/V2/Gift/Models/GiftModels.swift b/SodaLive/Sources/V2/Gift/Models/GiftModels.swift new file mode 100644 index 00000000..dd40c0a4 --- /dev/null +++ b/SodaLive/Sources/V2/Gift/Models/GiftModels.swift @@ -0,0 +1,135 @@ +import Foundation + +struct GiftFormOptionsResponse: Decodable { + let sizes: [GiftSizeOptionResponse] + let categories: [GiftCategoryOptionResponse] +} + +struct GiftSizeOptionResponse: Decodable { + let sizeCode: String + let name: String + let basePriceCan: Int + let salePriceCan: Int +} + +struct GiftCategoryOptionResponse: Decodable { + let categoryId: Int + let name: String + let requiresDamageWaiver: Bool +} + +struct CreateGiftRequest: Encodable { + let recipientMemberId: Int + let senderName: String + let senderPhoneNumber: String + let senderZipCode: String + let senderAddress: String + let senderAddressDetail: String + let sizeCode: String + let categoryId: Int + let senderTermsAgreed: Bool + let senderPrivacyAgreed: Bool + let damageWaiverAgreed: Bool +} + +struct CreateGiftResponse: Decodable { + let applicationNo: String + let status: String + let statusName: String + let priceCan: Int + let trackingDeadlineAt: String +} + +struct GiftListResponse: Decodable { + let totalCount: Int + let items: [GiftListItemResponse] + let page: Int + let size: Int + let hasNext: Bool +} + +struct GiftListItemResponse: Decodable { + let applicationNo: String + let direction: String + let status: String + let statusName: String + let priceCan: Int + let categoryName: String + let sizeName: String + let counterpartMemberId: Int + let counterpartNickname: String + let counterpartProfileImageUrl: String? + let createdAt: String +} + +struct GiftDetailResponse: Decodable { + let applicationNo: String + let direction: String + let status: String + let statusName: String + let giftInfo: GiftInfoResponse + let senderInfo: GiftSenderInfoResponse? + let recipientAddress: GiftRecipientAddressResponse? + let mailbox: GiftMailboxResponse? + let trackingRequired: Bool + let recipientAddressRequired: Bool + let recipientAddressDeadlineAt: String? + let delivery: GiftDeliveryResponse + let statusTimeline: [GiftStatusTimelineResponse] +} + +struct GiftInfoResponse: Decodable { + let recipientCreatorNickname: String? + let senderNickname: String? + let sizeName: String + let categoryName: String + let applicationNo: String? + let paidCan: Int? + let tracking: String? + let shippingRequestedAt: String? +} + +struct GiftSenderInfoResponse: Decodable { + let name: String + let phoneNumber: String + let address: String +} + +struct GiftRecipientAddressResponse: Decodable { + let name: String + let phoneNumber: String + let address: String +} + +struct GiftMailboxResponse: Decodable { + let name: String + let address: String + let phoneNumber: String +} + +struct GiftDeliveryResponse: Decodable { + let canceledAt: String? + let undeliverableAt: String? + let undeliverableReason: String? +} + +struct GiftStatusTimelineResponse: Decodable { + let status: String + let statusName: String + let occurredAt: String? +} + +struct RegisterGiftTrackingRequest: Encodable { + let courierCompanyName: String + let trackingNumber: String +} + +struct SubmitGiftRecipientAddressRequest: Encodable { + let recipientName: String + let recipientPhoneNumber: String + let recipientZipCode: String + let recipientAddress: String + let recipientAddressDetail: String + let recipientTermsAgreed: Bool + let recipientPrivacyAgreed: Bool +} diff --git a/work/tests/GiftDecodingContractCheck.swift b/work/tests/GiftDecodingContractCheck.swift new file mode 100644 index 00000000..93ab088e --- /dev/null +++ b/work/tests/GiftDecodingContractCheck.swift @@ -0,0 +1,62 @@ +import Foundation + +@main +enum GiftDecodingContractCheck { + static var assertions = 0 + + static func check(_ condition: Bool, _ message: String) { + guard condition else { print("FAIL: \(message)"); exit(1) } + assertions += 1 + } + + static func decode(_ type: T.Type, _ json: String) throws -> T { + try JSONDecoder().decode(type, from: Data(json.utf8)) + } + + static func main() { + let form = #"{"success":true,"message":"Options loaded","data":{"sizes":[{"sizeCode":"S","name":"Small","basePriceCan":100,"salePriceCan":80}],"categories":[{"categoryId":7,"name":"Flowers","requiresDamageWaiver":true}]}}"# + let create = #"{"success":true,"message":"Gift created","data":{"applicationNo":"G-1001","status":"CREATED","statusName":"Created","priceCan":80,"trackingDeadlineAt":"2026-10-10T12:00:00Z"}}"# + let list = #"{"success":true,"message":"Gifts loaded","data":{"totalCount":1,"items":[{"applicationNo":"G-1001","direction":"SENT","status":"CREATED","statusName":"Created","priceCan":80,"categoryName":"Flowers","sizeName":"Small","counterpartMemberId":42,"counterpartNickname":"Mina","counterpartProfileImageUrl":null,"createdAt":"2026-10-07T12:00:00Z"}],"page":1,"size":20,"hasNext":false}}"# + let detail = #"{"success":true,"message":"Gift detail loaded","data":{"applicationNo":"G-1001","direction":"SENT","status":"CREATED","statusName":"Created","giftInfo":{"recipientCreatorNickname":"Mina","senderNickname":"Jae","sizeName":"Small","categoryName":"Flowers","applicationNo":null,"paidCan":80,"tracking":"123456","shippingRequestedAt":null},"senderInfo":{"name":"Jae","phoneNumber":"010-1234-5678","address":"Seoul"},"recipientAddress":null,"mailbox":{"name":"Mina","address":"Busan","phoneNumber":"010-9876-5432"},"trackingRequired":true,"recipientAddressRequired":false,"recipientAddressDeadlineAt":null,"delivery":{"canceledAt":null,"undeliverableAt":null,"undeliverableReason":null},"statusTimeline":[{"status":"CREATED","statusName":"Created","occurredAt":"2026-10-07T12:00:00Z"}]}}"# + + let options = try! decode(ApiResponse.self, form) + check(options.success, "form success") + check(options.data?.sizes.first?.sizeCode == "S" && options.data?.sizes.first?.name == "Small", "size fields") + check(options.data?.categories.first?.categoryId == 7 && options.data?.categories.first?.name == "Flowers", "category fields") + check(options.data?.categories.first?.requiresDamageWaiver == true, "waiver") + + let created = try! decode(ApiResponse.self, create) + check(created.data?.applicationNo == "G-1001" && created.data?.statusName == "Created", "create fields") + check(created.data?.priceCan == 80 && created.data?.trackingDeadlineAt == "2026-10-10T12:00:00Z", "create values") + + let gifts = try! decode(ApiResponse.self, list) + check(gifts.data?.totalCount == 1 && gifts.data?.page == 1 && gifts.data?.size == 20 && gifts.data?.hasNext == false, "list paging") + check(gifts.data?.items.first?.categoryName == "Flowers" && gifts.data?.items.first?.counterpartNickname == "Mina", "list values") + check(gifts.data?.items.first?.counterpartProfileImageUrl == nil, "list nullable profile image") + + let gift = try! decode(ApiResponse.self, detail).data! + check(gift.applicationNo == "G-1001" && gift.giftInfo.categoryName == "Flowers", "detail values") + check(gift.applicationNo == "G-1001" && gift.giftInfo.applicationNo == nil, "detail application numbers") + check(gift.senderInfo?.name == "Jae" && gift.mailbox?.name == "Mina", "detail nested values") + check(gift.recipientAddress == nil && gift.recipientAddressDeadlineAt == nil && gift.giftInfo.shippingRequestedAt == nil, "detail nullable values") + check(gift.statusTimeline.first?.statusName == "Created", "timeline status name") + + for json in [ + #"{"success":true,"message":"Gift canceled","data":{"applicationNo":"G-1001","status":"CANCELED","statusName":"Canceled","priceCan":80,"canceledAt":"2026-10-07T13:00:00Z"}}"#, + #"{"success":true,"message":"Tracking registered","data":{"applicationNo":"G-1001","status":"TRACKING_REGISTERED","statusName":"Tracking registered","courierCompanyName":"Fast Express","trackingNumber":"123456","trackingRegisteredAt":"2026-10-07T13:00:00Z","recipientAddressDeadlineAt":"2026-10-10T12:00:00Z"}}"#, + #"{"success":true,"message":"Recipient address registered","data":{"applicationNo":"G-1001","status":"ADDRESS_REGISTERED","statusName":"Address registered","recipientAddressRegisteredAt":"2026-10-07T14:00:00Z"}}"# + ] { + check(try! decode(ApiResponseWithoutData.self, json).success, "mutation success envelope") + } + check(try! decode(ApiResponseWithoutData.self, #"{"success":true}"#).success, "envelope without data") + check(!(try! decode(ApiResponseWithoutData.self, #"{"success":false,"data":{"applicationNo":"G-1001"}}"#).success), "false success") + + let size = try! decode(GiftSizeOptionResponse.self, #"{"sizeCode":"S","name":"Small","basePriceCan":100,"salePriceCan":80}"#) + check(size.name == "Small", "direct size.name") + let category = try! decode(GiftCategoryOptionResponse.self, #"{"categoryId":7,"name":"Flowers","requiresDamageWaiver":true}"#) + check(category.name == "Flowers", "direct category.name") + let timeline = try! decode(GiftStatusTimelineResponse.self, #"{"status":"CREATED","statusName":"Created","occurredAt":"2026-10-07T12:00:00Z"}"#) + check(timeline.statusName == "Created", "direct timeline.statusName") + print("PASS: \(assertions) assertions") + } +} diff --git a/work/tests/check-gift-decoding-contract.sh b/work/tests/check-gift-decoding-contract.sh new file mode 100755 index 00000000..6ce389cc --- /dev/null +++ b/work/tests/check-gift-decoding-contract.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +swiftc \ + "$root/SodaLive/Sources/Common/ApiResponse.swift" \ + "$root/SodaLive/Sources/Common/ApiResponseWithoutData.swift" \ + "$root/SodaLive/Sources/V2/Gift/Models/GiftModels.swift" \ + "$root/work/tests/GiftDecodingContractCheck.swift" \ + -o "$tmp/gift-decoding-contract-check" + +"$tmp/gift-decoding-contract-check"