Files
ccn/Packages/Persistence/Sources/Public/Models/Listing.swift
T
javierandClaude Opus 5.5 911424de24 Project updates from Template
This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing:

- Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305)
- Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df)
- Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9)
- Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769)
- Enabled the Analytics trait in the Website service. (f6a6bbd)
- Reworked page routing in the Website service. (d1cb2c1)
- Gated the structured data features in the Infrastructure package behind a trait. (f0a8791)
- Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3)
- Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b)

The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead.

The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:55:43 +01:00

213 lines
6.6 KiB
Swift

import Foundation
/// A `Sendable` snapshot of one copy for sale, with the shop selling it.
public struct Listing: Sendable, Equatable {
// MARK: Properties
/// The listing's URL slug.
public let slug: String
/// The shop selling the copy.
public let shop: Shop
/// The listing's title, e.g. `Nightward #1`.
public let title: String
/// The series the copy belongs to, e.g. `Nightward (2019 series)`.
public let seriesTitle: String
/// The issue or volume, e.g. `#1, cover A`.
public let issueLabel: String
/// The publisher's name.
public let publisher: String
/// The cover year.
public let year: Int
/// The format token, e.g. `single-issues`.
public let format: String
/// The printing in a few words, e.g. `1st print`.
public let printing: String
/// The printing in full, e.g. `First printing, direct edition`.
public let printingDetail: String
/// Whether the copy is a first printing.
public let isFirstPrint: Bool
/// The company that graded and slabbed the copy, or `nil` for a raw copy.
public let grader: String?
/// The numeric grade, or `nil` for formats that are not graded.
public let grade: Double?
/// The condition as the seller states it, e.g. `NM` or `Like new`.
public let condition: String
/// The grader's certificate number, or `nil` for a raw copy.
public let certNumber: String?
/// The page quality, or `nil` when not stated.
public let pageQuality: String?
/// Why the issue is a key, or `nil` when it is not one.
public let keyNotes: String?
/// The seller's notes on the copy.
public let sellerNotes: String
/// The asking price, in euro cents.
public let priceCents: Int
/// The shipping cost to the Netherlands, in euro cents.
public let shippingCents: Int
/// Whether the seller takes offers on the copy.
public let acceptsOffers: Bool
/// The placeholder cover shown until listings carry photos.
public let cover: Cover
/// When the copy was listed.
public let listedAt: Date
/// When a sale of the copy was agreed, or `nil` while it is for sale.
public let soldAt: Date?
/// The defects the seller declared, from the grading guide's checklist.
public let defects: [String]
/// The URL paths of the photos of this copy, the front first; empty while the listing shows its placeholder cover.
public let photos: [String]
// MARK: Initializers
public init(
slug: String,
shop: Shop,
title: String,
seriesTitle: String,
issueLabel: String,
publisher: String,
year: Int,
format: String,
printing: String,
printingDetail: String,
isFirstPrint: Bool,
grader: String?,
grade: Double?,
condition: String,
certNumber: String?,
pageQuality: String?,
keyNotes: String?,
sellerNotes: String,
priceCents: Int,
shippingCents: Int,
acceptsOffers: Bool,
cover: Cover,
listedAt: Date,
soldAt: Date? = nil,
defects: [String] = [],
photos: [String] = []
) {
self.slug = slug
self.shop = shop
self.title = title
self.seriesTitle = seriesTitle
self.issueLabel = issueLabel
self.publisher = publisher
self.year = year
self.format = format
self.printing = printing
self.printingDetail = printingDetail
self.isFirstPrint = isFirstPrint
self.grader = grader
self.grade = grade
self.condition = condition
self.certNumber = certNumber
self.pageQuality = pageQuality
self.keyNotes = keyNotes
self.sellerNotes = sellerNotes
self.priceCents = priceCents
self.shippingCents = shippingCents
self.acceptsOffers = acceptsOffers
self.cover = cover
self.listedAt = listedAt
self.soldAt = soldAt
self.defects = defects
self.photos = photos
}
}
// MARK: - Extensions
public extension Listing {
// MARK: Computed
/// What the seller declared of the copy's condition, in a sentence: the defects, then the notes. Frozen into a sale at agreement.
var declaration: String {
let defects = defects.isEmpty
? "No defects declared."
: "Declared defects: \(defects.joined(separator: "; "))."
return sellerNotes.isEmpty ? defects : "\(defects) \(sellerNotes)"
}
/// The grade or condition in a few words: `CGC 9.8` for a slab, `Raw · NM` for a raw issue, the condition alone for a collected edition.
var gradeLabel: String {
if let grader, let grade {
return "\(grader) \(grade.formatted(.number.precision(.fractionLength(1)).locale(.init(identifier: "en"))))"
}
return format == "single-issues" ? "Raw · \(condition)" : condition
}
/// Whether the copy is still for sale.
var isAvailable: Bool {
soldAt == nil
}
/// Whether the issue is a key.
var isKey: Bool {
keyNotes != nil
}
/// The issue's number within its series, or `nil` for a copy that is not a numbered single issue.
var issueNumber: Int? {
format == "single-issues"
? Int(issueLabel.drop { !$0.isNumber }.prefix { $0.isNumber })
: nil
}
/// The name of the series the copy belongs to: its series title, less the `(2019 series)` a single issue's carries.
var seriesName: String {
seriesTitle
.replacing(#/\s\(\d{4} series\)$/#, with: "")
}
/// The slug of the series the copy belongs to.
var seriesSlug: String {
ListingDraft.slug(seriesName)
}
/// Whether the copy is graded and slabbed by a grading company.
var isSlabbed: Bool {
grader != nil
}
}
// MARK: - Structs
public extension Listing {
/// A placeholder cover: lettering on a toned ground.
struct Cover: Sendable, Equatable {
// MARK: Properties
/// The tone token naming the cover's colours, e.g. `navy`.
public let tone: String
/// The title lettering, one line per newline.
public let title: String
/// The publisher's imprint line.
public let imprint: String
/// The issue number or volume marker.
public let number: String
// MARK: Initializers
public init(
tone: String,
title: String,
imprint: String,
number: String
) {
self.tone = tone
self.title = title
self.imprint = imprint
self.number = number
}
}
}