Files
ccn/Packages/Persistence/Sources/Public/Models/Sale.swift
T

224 lines
6.6 KiB
Swift
Raw Normal View History

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
}
}