import Foundation /// A `Sendable` snapshot of a sale record: what was agreed, and how far its handshake has come. /// /// The handshake has five steps — agreed, paid, sent, received, rated — each stamped by the person who took it. Payment is settled between /// buyer and seller off-platform; the record keeps the amounts and the stamps. public struct Sale: Sendable, Equatable { // MARK: Properties /// The record's number, shown as `CCN-48213`. public let number: Int /// The identifier of the conversation the sale was agreed in. public let conversationID: UUID /// The listing sold. public let listing: Listing /// The account buying. public let buyer: User /// The account selling: the owner of the listing's shop. public let seller: User /// The agreed price, in euro cents. public let priceCents: Int /// The asking price at the moment of agreement, in euro cents. public let askedCents: Int /// The agreed shipping cost, in euro cents. public let shippingCents: Int /// The condition the seller declared, frozen at the moment of agreement. public let declared: Declaration /// When the sale was agreed. public let agreedAt: Date /// When the buyer marked the sale paid. public let paidAt: Date? /// How the buyer says they paid. public let paymentNote: String? /// When the seller marked the copy sent. public let sentAt: Date? /// The carrier and tracking the seller gave. public let shippingNote: String? /// When the buyer marked the copy received. public let receivedAt: Date? /// The stars the buyer gave the seller, out of five. public let buyerRating: Int? /// The stars the seller gave the buyer, out of five. public let sellerRating: Int? // MARK: Initializers public init( number: Int, conversationID: UUID, listing: Listing, buyer: User, seller: User, priceCents: Int, askedCents: Int, shippingCents: Int, declared: Declaration, agreedAt: Date, paidAt: Date? = nil, paymentNote: String? = nil, sentAt: Date? = nil, shippingNote: String? = nil, receivedAt: Date? = nil, buyerRating: Int? = nil, sellerRating: Int? = nil ) { self.number = number self.conversationID = conversationID self.listing = listing self.buyer = buyer self.seller = seller self.priceCents = priceCents self.askedCents = askedCents self.shippingCents = shippingCents self.declared = declared self.agreedAt = agreedAt self.paidAt = paidAt self.paymentNote = paymentNote self.sentAt = sentAt self.shippingNote = shippingNote self.receivedAt = receivedAt self.buyerRating = buyerRating self.sellerRating = sellerRating } } // MARK: - Structs public extension Sale { /// The condition a seller declared, frozen when the sale was agreed: a problem report is only ever for what is not in it. struct Declaration: Sendable, Equatable { /// The grade or condition, e.g. `CGC 9.8`. public let grade: String /// The page quality, or `nil` when not stated. public let pageQuality: String? /// The printing in full. public let printing: String /// The seller's notes on the copy. public let notes: String public init( grade: String, pageQuality: String?, printing: String, notes: String ) { self.grade = grade self.pageQuality = pageQuality self.printing = printing self.notes = notes } } } // MARK: - Enumerations public extension Sale { /// Something a party does to move the handshake on. enum Action: Sendable, Equatable { /// The buyer says they paid, and how. case markPaid(note: String) /// The seller says the copy is on its way, and how. case markSent(note: String) /// The buyer says the copy arrived. case markReceived /// Either party rates the other, out of five. case rate(stars: Int) } /// A party to a sale. enum Role: Sendable { case buyer case seller } /// A step of the handshake, in order. enum Step: Int, CaseIterable, Comparable, Sendable { case agreed = 1 case paid case sent case received case rated public static func < (lhs: Self, rhs: Self) -> Bool { lhs.rawValue < rhs.rawValue } } } // MARK: - Extensions public extension Sale { // MARK: Computed /// The last step taken. Rated counts once both parties have rated. var completed: Step { if buyerRating != nil, sellerRating != nil { return .rated } if receivedAt != nil { return .received } if sentAt != nil { return .sent } return paidAt != nil ? .paid : .agreed } /// The step waited on, or `nil` once the handshake is done. var next: Step? { .init(rawValue: completed.rawValue + 1) } /// The agreed price and shipping together, in euro cents. var totalCents: Int { priceCents + shippingCents } // MARK: Methods /// Whether a party may take an action now. /// /// Each step waits for the one before it and is taken once, by the party it belongs to: the buyer pays and receives, the seller sends. /// Ratings unlock only after the copy is received, so every rating maps to a finished sale. /// - Parameters: /// - action: the action to take. /// - role: the party taking it. func allows( _ action: Action, by role: Role ) -> Bool { switch action { case .markPaid: role == .buyer && completed == .agreed case .markSent: role == .seller && completed == .paid case .markReceived: role == .buyer && completed == .sent case .rate(let stars): receivedAt != nil && (1...5).contains(stars) && rating(by: role) == nil } } /// The stars a party gave, or `nil` when they have not rated yet. func rating( by role: Role ) -> Int? { role == .buyer ? buyerRating : sellerRating } /// The party an account is to the sale, or `nil` when it is neither. /// - Parameter userID: the account's identifier. func role( of userID: UUID ) -> Role? { if userID == buyer.id { return .buyer } return userID == seller.id ? .seller : nil } }