224 lines
6.6 KiB
Swift
224 lines
6.6 KiB
Swift
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
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|