From 01bd8199959fedde174a18396ea4bbccadfbf437 Mon Sep 17 00:00:00 2001
From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com>
Date: Tue, 17 Dec 2024 17:37:04 +0100
Subject: [PATCH] Remove delegates (#18)

- Completely remove the delegates and move their methods to `PassDataModel` and `OrderDataModel`
- Provide a default implementation for the model middleware of `PassDataModel` and `OrderDataModel`
- Update DocC
---
 Package.swift                                 |   7 +-
 Sources/Orders/DTOs/OrderJSON.swift           |   7 -
 Sources/Orders/DTOs/OrdersForDeviceDTO.swift  |   7 -
 .../Middleware/AppleOrderMiddleware.swift     |   7 -
 .../OrdersService+AsyncModelMiddleware.swift  |  42 ++++++
 .../Orders/Models/Concrete Models/Order.swift |   7 -
 .../Models/Concrete Models/OrdersDevice.swift |   7 -
 .../Concrete Models/OrdersErrorLog.swift      |   7 -
 .../Concrete Models/OrdersRegistration.swift  |   7 -
 Sources/Orders/Models/OrderDataModel.swift    |  34 ++++-
 Sources/Orders/Models/OrderModel.swift        |  13 +-
 .../Models/OrdersRegistrationModel.swift      |   7 -
 Sources/Orders/Orders.docc/GettingStarted.md  | 123 +++++------------
 Sources/Orders/Orders.docc/Orders.md          |   3 +-
 Sources/Orders/OrdersDelegate.swift           |  44 ------
 Sources/Orders/OrdersService.swift            |  18 +--
 Sources/Orders/OrdersServiceCustom.swift      |  76 +++++------
 Sources/PassKit/Models/DeviceModel.swift      |   4 +-
 Sources/PassKit/Models/ErrorLogModel.swift    |   4 +-
 Sources/PassKit/WalletError.swift             |   7 -
 Sources/Passes/DTOs/PassJSON.swift            |   7 -
 .../DTOs/PersonalizationDictionaryDTO.swift   |   7 -
 Sources/Passes/DTOs/PersonalizationJSON.swift |   6 +-
 .../PassesService+AsyncModelMiddleware.swift  |  42 ++++++
 .../Passes/Models/Concrete Models/Pass.swift  |   9 +-
 .../Models/Concrete Models/PassesDevice.swift |  11 +-
 .../Concrete Models/PassesErrorLog.swift      |  11 +-
 .../Concrete Models/PassesRegistration.swift  |   7 -
 .../Concrete Models/UserPersonalization.swift |   7 -
 Sources/Passes/Models/PassDataModel.swift     |  47 ++++++-
 Sources/Passes/Models/PassModel.swift         |   2 +-
 .../Models/UserPersonalizationModel.swift     |   7 -
 Sources/Passes/Passes.docc/GettingStarted.md  | 126 +++++-------------
 Sources/Passes/Passes.docc/Passes.md          |   3 +-
 Sources/Passes/Passes.docc/Personalization.md |  62 +++------
 Sources/Passes/PassesDelegate.swift           |  87 ------------
 Sources/Passes/PassesService.swift            |  19 ++-
 Sources/Passes/PassesServiceCustom.swift      |  90 ++++++-------
 Tests/OrdersTests/OrderData.swift             | 122 -----------------
 Tests/OrdersTests/OrdersTests.swift           |   9 +-
 Tests/OrdersTests/TestOrdersDelegate.swift    |  24 ----
 Tests/OrdersTests/Utils/OrderData.swift       |  57 ++++++++
 Tests/OrdersTests/Utils/OrderJSONData.swift   |  51 +++++++
 Tests/OrdersTests/{ => Utils}/withApp.swift   |  11 +-
 Tests/PassesTests/PassesTests.swift           |  39 ++----
 Tests/PassesTests/TestPassesDelegate.swift    |  46 -------
 Tests/PassesTests/Utils/PassData.swift        |  70 ++++++++++
 .../PassJSONData.swift}                       |  74 +---------
 Tests/PassesTests/{ => Utils}/withApp.swift   |  11 +-
 49 files changed, 562 insertions(+), 933 deletions(-)
 create mode 100644 Sources/Orders/Middleware/OrdersService+AsyncModelMiddleware.swift
 delete mode 100644 Sources/Orders/OrdersDelegate.swift
 create mode 100644 Sources/Passes/Middleware/PassesService+AsyncModelMiddleware.swift
 delete mode 100644 Sources/Passes/PassesDelegate.swift
 delete mode 100644 Tests/OrdersTests/OrderData.swift
 delete mode 100644 Tests/OrdersTests/TestOrdersDelegate.swift
 create mode 100644 Tests/OrdersTests/Utils/OrderData.swift
 create mode 100644 Tests/OrdersTests/Utils/OrderJSONData.swift
 rename Tests/OrdersTests/{ => Utils}/withApp.swift (77%)
 delete mode 100644 Tests/PassesTests/TestPassesDelegate.swift
 create mode 100644 Tests/PassesTests/Utils/PassData.swift
 rename Tests/PassesTests/{PassData.swift => Utils/PassJSONData.swift} (54%)
 rename Tests/PassesTests/{ => Utils}/withApp.swift (77%)

diff --git a/Package.swift b/Package.swift
index 4bfb12f..cdbc96a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -4,14 +4,14 @@ import PackageDescription
 let package = Package(
     name: "PassKit",
     platforms: [
-        .macOS(.v14)
+        .macOS(.v13)
     ],
     products: [
         .library(name: "Passes", targets: ["Passes"]),
         .library(name: "Orders", targets: ["Orders"]),
     ],
     dependencies: [
-        .package(url: "https://github.com/vapor/vapor.git", from: "4.106.1"),
+        .package(url: "https://github.com/vapor/vapor.git", from: "4.108.0"),
         .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"),
         .package(url: "https://github.com/vapor/apns.git", from: "4.2.0"),
         .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.4"),
@@ -76,7 +76,6 @@ let package = Package(
 
 var swiftSettings: [SwiftSetting] {
     [
-        .enableUpcomingFeature("ExistentialAny"),
-        .enableUpcomingFeature("FullTypedThrows"),
+        .enableUpcomingFeature("ExistentialAny")
     ]
 }
diff --git a/Sources/Orders/DTOs/OrderJSON.swift b/Sources/Orders/DTOs/OrderJSON.swift
index 23b7afb..66bfea0 100644
--- a/Sources/Orders/DTOs/OrderJSON.swift
+++ b/Sources/Orders/DTOs/OrderJSON.swift
@@ -1,10 +1,3 @@
-//
-//  OrderJSON.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 02/07/24.
-//
-
 /// The structure of a `order.json` file.
 public struct OrderJSON {
     /// A protocol that defines the structure of a `order.json` file.
diff --git a/Sources/Orders/DTOs/OrdersForDeviceDTO.swift b/Sources/Orders/DTOs/OrdersForDeviceDTO.swift
index 6ef7f64..4891d2e 100644
--- a/Sources/Orders/DTOs/OrdersForDeviceDTO.swift
+++ b/Sources/Orders/DTOs/OrdersForDeviceDTO.swift
@@ -1,10 +1,3 @@
-//
-//  OrdersForDeviceDTO.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import Vapor
 
 struct OrdersForDeviceDTO: Content {
diff --git a/Sources/Orders/Middleware/AppleOrderMiddleware.swift b/Sources/Orders/Middleware/AppleOrderMiddleware.swift
index 66d8544..2f8f83b 100644
--- a/Sources/Orders/Middleware/AppleOrderMiddleware.swift
+++ b/Sources/Orders/Middleware/AppleOrderMiddleware.swift
@@ -1,10 +1,3 @@
-//
-//  AppleOrderMiddleware.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 import Vapor
 
diff --git a/Sources/Orders/Middleware/OrdersService+AsyncModelMiddleware.swift b/Sources/Orders/Middleware/OrdersService+AsyncModelMiddleware.swift
new file mode 100644
index 0000000..0a845b1
--- /dev/null
+++ b/Sources/Orders/Middleware/OrdersService+AsyncModelMiddleware.swift
@@ -0,0 +1,42 @@
+import FluentKit
+import Foundation
+
+extension OrdersService: AsyncModelMiddleware {
+    public func create(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let order = Order(
+            typeIdentifier: OD.typeIdentifier,
+            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
+        )
+        try await order.save(on: db)
+        model._$order.id = try order.requireID()
+        try await next.create(model, on: db)
+    }
+
+    public func update(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let order = try await model._$order.get(on: db)
+        order.updatedAt = Date.now
+        try await order.save(on: db)
+        try await next.update(model, on: db)
+        try await self.sendPushNotifications(for: model, on: db)
+    }
+}
+
+extension OrdersServiceCustom: AsyncModelMiddleware {
+    public func create(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let order = O(
+            typeIdentifier: OD.typeIdentifier,
+            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
+        )
+        try await order.save(on: db)
+        model._$order.id = try order.requireID()
+        try await next.create(model, on: db)
+    }
+
+    public func update(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let order = try await model._$order.get(on: db)
+        order.updatedAt = Date.now
+        try await order.save(on: db)
+        try await next.update(model, on: db)
+        try await self.sendPushNotifications(for: model, on: db)
+    }
+}
diff --git a/Sources/Orders/Models/Concrete Models/Order.swift b/Sources/Orders/Models/Concrete Models/Order.swift
index 8cf29ef..529e48a 100644
--- a/Sources/Orders/Models/Concrete Models/Order.swift	
+++ b/Sources/Orders/Models/Concrete Models/Order.swift	
@@ -1,10 +1,3 @@
-//
-//  Order.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 import Foundation
 
diff --git a/Sources/Orders/Models/Concrete Models/OrdersDevice.swift b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift
index b739e94..046b293 100644
--- a/Sources/Orders/Models/Concrete Models/OrdersDevice.swift	
+++ b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift	
@@ -1,10 +1,3 @@
-//
-//  OrdersDevice.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 import PassKit
 
diff --git a/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift b/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift
index d0b7ac6..3fd5571 100644
--- a/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift	
+++ b/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift	
@@ -1,10 +1,3 @@
-//
-//  OrdersErrorLog.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 import PassKit
 
diff --git a/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift b/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift
index ea4ddf3..6b17af5 100644
--- a/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift	
+++ b/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift	
@@ -1,10 +1,3 @@
-//
-//  OrdersRegistration.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 
 /// The `Model` that stores orders registrations.
diff --git a/Sources/Orders/Models/OrderDataModel.swift b/Sources/Orders/Models/OrderDataModel.swift
index cbb89f1..6558fc0 100644
--- a/Sources/Orders/Models/OrderDataModel.swift
+++ b/Sources/Orders/Models/OrderDataModel.swift
@@ -1,18 +1,38 @@
-//
-//  OrderDataModel.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 
 /// Represents the `Model` that stores custom app data associated to Wallet orders.
 public protocol OrderDataModel: Model {
     associatedtype OrderType: OrderModel
 
+    /// An identifier for the order type associated with the order.
+    static var typeIdentifier: String { get }
+
     /// The foreign key to the order table.
     var order: OrderType { get set }
+
+    /// Encode the order into JSON.
+    ///
+    /// This method should generate the entire order JSON.
+    ///
+    /// - Parameter db: The SQL database to query against.
+    ///
+    /// - Returns: An object that conforms to ``OrderJSON/Properties``.
+    ///
+    /// > Tip: See the [`Order`](https://developer.apple.com/documentation/walletorders/order) object to understand the keys.
+    func orderJSON(on db: any Database) async throws -> any OrderJSON.Properties
+
+    /// Should return a URL path which points to the template data for the order.
+    ///
+    /// The path should point to a directory containing all the images and localizations for the generated `.order` archive
+    /// but should *not* contain any of these items:
+    ///  - `manifest.json`
+    ///  - `order.json`
+    ///  - `signature`
+    ///
+    /// - Parameter db: The SQL database to query against.
+    ///
+    /// - Returns: A URL path which points to the template data for the order.
+    func template(on db: any Database) async throws -> String
 }
 
 extension OrderDataModel {
diff --git a/Sources/Orders/Models/OrderModel.swift b/Sources/Orders/Models/OrderModel.swift
index 92cbd4f..803cb94 100644
--- a/Sources/Orders/Models/OrderModel.swift
+++ b/Sources/Orders/Models/OrderModel.swift
@@ -1,10 +1,3 @@
-//
-//  OrderModel.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 import Foundation
 
@@ -23,6 +16,12 @@ public protocol OrderModel: Model where IDValue == UUID {
 
     /// The authentication token supplied to your web service.
     var authenticationToken: String { get set }
+
+    /// The designated initializer.
+    /// - Parameters:
+    ///   - typeIdentifier: The order type identifier that’s registered with Apple.
+    ///   - authenticationToken: The authentication token to use with the web service in the `webServiceURL` key.
+    init(typeIdentifier: String, authenticationToken: String)
 }
 
 extension OrderModel {
diff --git a/Sources/Orders/Models/OrdersRegistrationModel.swift b/Sources/Orders/Models/OrdersRegistrationModel.swift
index 22a8708..89ed421 100644
--- a/Sources/Orders/Models/OrdersRegistrationModel.swift
+++ b/Sources/Orders/Models/OrdersRegistrationModel.swift
@@ -1,10 +1,3 @@
-//
-//  OrdersRegistrationModel.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 30/06/24.
-//
-
 import FluentKit
 import PassKit
 
diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md
index ec20edf..e015b74 100644
--- a/Sources/Orders/Orders.docc/GettingStarted.md
+++ b/Sources/Orders/Orders.docc/GettingStarted.md
@@ -9,21 +9,23 @@ For all the other custom data needed to generate the order, such as the barcodes
 The order data model will be used to generate the `order.json` file contents.
 
 The order you distribute to a user is a signed bundle that contains the `order.json` file, images, and optional localizations.
-The Orders framework provides the ``OrdersService`` class that handles the creation of the order JSON file and the signing of the order bundle, using an ``OrdersDelegate`` that you must implement.
+The Orders framework provides the ``OrdersService`` class that handles the creation of the order JSON file and the signing of the order bundle.
 The ``OrdersService`` class also provides methods to send push notifications to all devices registered when you update an order, and all the routes that Apple Wallet uses to retrieve orders.
 
 ### Implement the Order Data Model
 
-Your data model should contain all the fields that you store for your order, as well as a foreign key to ``Order``, the order model offered by the Orders framework.
+Your data model should contain all the fields that you store for your order, as well as a foreign key to ``Order``, the order model offered by the Orders framework, and a order type identifier that's registered with Apple.
 
 ```swift
 import Fluent
-import struct Foundation.UUID
+import Foundation
 import Orders
 
 final class OrderData: OrderDataModel, @unchecked Sendable {
     static let schema = "order_data"
 
+    static let typeIdentifier = Environment.get("ORDER_TYPE_IDENTIFIER")!
+
     @ID
     var id: UUID?
 
@@ -54,6 +56,23 @@ struct CreateOrderData: AsyncMigration {
 }
 ```
 
+You also have to define two methods in the ``OrderDataModel``:
+- ``OrderDataModel/orderJSON(on:)``, where you'll have to return a `struct` that conforms to ``OrderJSON/Properties``.
+- ``OrderDataModel/template(on:)``, where you'll have to return the path to a folder containing the order files.
+
+```swift
+extension OrderData {
+    func orderJSON(on db: any Database) async throws -> any OrderJSON.Properties {
+        try await OrderJSONData(data: self, order: self.$order.get(on: db))
+    }
+
+    func template(on db: any Database) async throws -> String {
+        // The location might vary depending on the type of order.
+        "Templates/Orders/"
+    }
+}
+```
+
 ### Handle Cleanup
 
 Depending on your implementation details, you may want to automatically clean out the orders and devices table when a registration is deleted.
@@ -73,7 +92,7 @@ import Orders
 
 struct OrderJSONData: OrderJSON.Properties {
     let schemaVersion = OrderJSON.SchemaVersion.v1
-    let orderTypeIdentifier = Environment.get("ORDER_TYPE_IDENTIFIER")!
+    let orderTypeIdentifier = OrderData.typeIdentifier
     let orderIdentifier: String
     let orderType = OrderJSON.OrderType.ecommerce
     let orderNumber = "HM090772020864"
@@ -108,41 +127,6 @@ struct OrderJSONData: OrderJSON.Properties {
 
 > Important: You **must** add `api/orders/` to your `webServiceURL`, as shown in the example above.
 
-### Implement the Delegate
-
-Create a delegate class that implements ``OrdersDelegate``.
-
-Because the files for your order's template and the method of encoding might vary by order type, you'll be provided the ``Order`` for those methods.
-In the ``OrdersDelegate/encode(order:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``OrderJSON``.
-
-```swift
-import Vapor
-import Fluent
-import Orders
-
-final class OrderDelegate: OrdersDelegate {
-    func encode<O: OrderModel>(order: O, db: Database, encoder: JSONEncoder) async throws -> Data {
-        // The specific OrderData class you use here may vary based on the `order.typeIdentifier`
-        // if you have multiple different types of orders, and thus multiple types of order data.
-        guard let orderData = try await OrderData.query(on: db)
-            .filter(\.$order.$id == order.requireID())
-            .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-        guard let data = try? encoder.encode(OrderJSONData(data: orderData, order: order)) else {
-            throw Abort(.internalServerError)
-        }
-        return data
-    }
-
-    func template<O: OrderModel>(for order: O, db: Database) async throws -> String {
-        // The location might vary depending on the type of order.
-        "Templates/Orders/"
-    }
-}
-```
-
 ### Initialize the Service
 
 Next, initialize the ``OrdersService`` inside the `configure.swift` file.
@@ -155,13 +139,10 @@ import Fluent
 import Vapor
 import Orders
 
-let orderDelegate = OrderDelegate()
-
 public func configure(_ app: Application) async throws {
     ...
-    let ordersService = try OrdersService(
+    let ordersService = try OrdersService<OrderData>(
         app: app,
-        delegate: orderDelegate,
         pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!,
         pemCertificate: Environment.get("PEM_CERTIFICATE")!,
         pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")!
@@ -169,8 +150,6 @@ public func configure(_ app: Application) async throws {
 }
 ```
 
-> Note: Notice how the ``OrdersDelegate`` is created as a global variable. You need to ensure that the delegate doesn't go out of scope as soon as the `configure(_:)` method exits.
-
 If you wish to include routes specifically for sending push notifications to updated orders, you can also pass to the ``OrdersService`` initializer whatever `Middleware` you want Vapor to use to authenticate the two routes. Doing so will add two routes, the first one sends notifications and the second one retrieves a list of push tokens which would be sent a notification.
 
 ```http
@@ -191,18 +170,16 @@ import Vapor
 import PassKit
 import Orders
 
-let orderDelegate = OrderDelegate()
-
 public func configure(_ app: Application) async throws {
     ...
     let ordersService = try OrdersServiceCustom<
+        OrderData,
         MyOrderType,
         MyDeviceType,
         MyOrdersRegistrationType,
         MyErrorLogType
     >(
         app: app,
-        delegate: orderDelegate,
         pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!,
         pemCertificate: Environment.get("PEM_CERTIFICATE")!,
         pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")!
@@ -215,56 +192,25 @@ public func configure(_ app: Application) async throws {
 If you're using the default schemas provided by this framework, you can register the default models in your `configure(_:)` method:
 
 ```swift
-OrdersService.register(migrations: app.migrations)
+OrdersService<OrderData>.register(migrations: app.migrations)
 ```
 
 > Important: Register the default models before the migration of your order data model.
 
 ### Order Data Model Middleware
 
-You'll want to create a model middleware to handle the creation and update of the order data model.
-This middleware could be responsible for creating and linking an ``Order`` to the order data model, depending on your requirements.
-When your order data changes, it should also update the ``Order/updatedAt`` field of the ``Order`` and send a push notification to all devices registered to that order.
+This framework provides a model middleware to handle the creation and update of the order data model.
 
-```swift
-import Vapor
-import Fluent
-import Orders
-
-struct OrderDataMiddleware: AsyncModelMiddleware {
-    private unowned let service: OrdersService
-
-    init(service: OrdersService) {
-        self.service = service
-    }
-
-    // Create the `Order` and add it to the `OrderData` automatically at creation
-    func create(model: OrderData, on db: Database, next: AnyAsyncModelResponder) async throws {
-        let order = Order(
-            typeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
-            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString())
-        try await order.save(on: db)
-        model.$order.id = try order.requireID()
-        try await next.create(model, on: db)
-    }
-
-    func update(model: OrderData, on db: Database, next: AnyAsyncModelResponder) async throws {
-        let order = try await model.$order.get(on: db)
-        order.updatedAt = Date()
-        try await order.save(on: db)
-        try await next.update(model, on: db)
-        try await service.sendPushNotifications(for: order, on: db)
-    }
-}
-```
+When you create an ``OrderDataModel`` object, it will automatically create an ``OrderModel`` object with a random auth token and the correct type identifier and link it to the order data model.
+When you update an order data model, it will update the ``OrderModel`` object and send a push notification to all devices registered to that order.
 
-You could register it in the `configure.swift` file.
+You can register it like so (either with an ``OrdersService`` or an ``OrdersServiceCustom``):
 
 ```swift
-app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .psql)
+app.databases.middleware.use(ordersService, on: .psql)
 ```
 
-> Important: Whenever your order data changes, you must update the ``Order/updatedAt`` time of the linked ``Order`` so that Wallet knows to retrieve a new order.
+> Note: If you don't like the default implementation of the model middleware, it is highly recommended that you create your own. But remember: whenever your order data changes, you must update the ``Order/updatedAt`` time of the linked ``Order`` so that Wallet knows to retrieve a new order.
 
 ### Generate the Order Content
 
@@ -291,15 +237,14 @@ Then use the object inside your route handlers to generate the order bundle with
 ```swift
 fileprivate func orderHandler(_ req: Request) async throws -> Response {
     ...
-    guard let orderData = try await OrderData.query(on: req.db)
+    guard let order = try await OrderData.query(on: req.db)
         .filter(...)
-        .with(\.$order)
         .first()
     else {
         throw Abort(.notFound)
     }
 
-    let bundle = try await ordersService.build(order: orderData.order, on: req.db)
+    let bundle = try await ordersService.build(order: order, on: req.db)
     let body = Response.Body(data: bundle)
     var headers = HTTPHeaders()
     headers.add(name: .contentType, value: "application/vnd.apple.order")
diff --git a/Sources/Orders/Orders.docc/Orders.md b/Sources/Orders/Orders.docc/Orders.md
index 952a12b..60d6d52 100644
--- a/Sources/Orders/Orders.docc/Orders.md
+++ b/Sources/Orders/Orders.docc/Orders.md
@@ -14,11 +14,11 @@ For information on Apple Wallet orders, see the [Apple Developer Documentation](
 ### Essentials
 
 - <doc:GettingStarted>
+- ``OrderDataModel``
 - ``OrderJSON``
 
 ### Building and Distribution
 
-- ``OrdersDelegate``
 - ``OrdersService``
 - ``OrdersServiceCustom``
 
@@ -33,4 +33,3 @@ For information on Apple Wallet orders, see the [Apple Developer Documentation](
 
 - ``OrderModel``
 - ``OrdersRegistrationModel``
-- ``OrderDataModel``
diff --git a/Sources/Orders/OrdersDelegate.swift b/Sources/Orders/OrdersDelegate.swift
deleted file mode 100644
index e63979c..0000000
--- a/Sources/Orders/OrdersDelegate.swift
+++ /dev/null
@@ -1,44 +0,0 @@
-//
-//  OrdersDelegate.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 01/07/24.
-//
-
-import FluentKit
-import Foundation
-
-/// The delegate which is responsible for generating the order files.
-public protocol OrdersDelegate: AnyObject, Sendable {
-    /// Should return a URL path which points to the template data for the order.
-    ///
-    /// The path should point to a directory containing all the images and localizations for the generated `.order` archive
-    /// but should *not* contain any of these items:
-    ///  - `manifest.json`
-    ///  - `order.json`
-    ///  - `signature`
-    ///
-    /// - Parameters:
-    ///   - order: The order data from the SQL server.
-    ///   - db: The SQL database to query against.
-    ///
-    /// - Returns: A URL path which points to the template data for the order.
-    func template<O: OrderModel>(for order: O, db: any Database) async throws -> String
-
-    /// Encode the order into JSON.
-    ///
-    /// This method should generate the entire order JSON. You are provided with
-    /// the order data from the SQL database and you should return a properly
-    /// formatted order file encoding.
-    ///
-    /// - Parameters:
-    ///   - order: The order data from the SQL server
-    ///   - db: The SQL database to query against.
-    ///   - encoder: The `JSONEncoder` which you should use.
-    /// - Returns: The encoded order JSON data.
-    ///
-    /// > Tip: See the [`Order`](https://developer.apple.com/documentation/walletorders/order) object to understand the keys.
-    func encode<O: OrderModel>(
-        order: O, db: any Database, encoder: JSONEncoder
-    ) async throws -> Data
-}
diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift
index e915472..33220d5 100644
--- a/Sources/Orders/OrdersService.swift
+++ b/Sources/Orders/OrdersService.swift
@@ -1,22 +1,14 @@
-//
-//  OrdersService.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 01/07/24.
-//
-
 import FluentKit
 import Vapor
 
 /// The main class that handles Wallet orders.
-public final class OrdersService: Sendable {
-    private let service: OrdersServiceCustom<Order, OrdersDevice, OrdersRegistration, OrdersErrorLog>
+public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD.OrderType {
+    private let service: OrdersServiceCustom<OD, Order, OrdersDevice, OrdersRegistration, OrdersErrorLog>
 
     /// Initializes the service and registers all the routes required for Apple Wallet to work.
     ///
     /// - Parameters:
     ///   - app: The `Vapor.Application` to use in route handlers and APNs.
-    ///   - delegate: The ``OrdersDelegate`` to use for order generation.
     ///   - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
     ///   - logger: The `Logger` to use.
     ///   - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format.
@@ -26,7 +18,6 @@ public final class OrdersService: Sendable {
     ///   - openSSLPath: The location of the `openssl` command as a file path.
     public init(
         app: Application,
-        delegate: any OrdersDelegate,
         pushRoutesMiddleware: (any Middleware)? = nil,
         logger: Logger? = nil,
         pemWWDRCertificate: String,
@@ -37,7 +28,6 @@ public final class OrdersService: Sendable {
     ) throws {
         self.service = try .init(
             app: app,
-            delegate: delegate,
             pushRoutesMiddleware: pushRoutesMiddleware,
             logger: logger,
             pemWWDRCertificate: pemWWDRCertificate,
@@ -55,7 +45,7 @@ public final class OrdersService: Sendable {
     ///   - db: The `Database` to use.
     ///
     /// - Returns: The generated order content.
-    public func build(order: Order, on db: any Database) async throws -> Data {
+    public func build(order: OD, on db: any Database) async throws -> Data {
         try await service.build(order: order, on: db)
     }
 
@@ -74,7 +64,7 @@ public final class OrdersService: Sendable {
     /// - Parameters:
     ///   - order: The order to send the notifications for.
     ///   - db: The `Database` to use.
-    public func sendPushNotifications(for order: Order, on db: any Database) async throws {
+    public func sendPushNotifications(for order: OD, on db: any Database) async throws {
         try await service.sendPushNotifications(for: order, on: db)
     }
 }
diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift
index 3dde8f0..2f17d22 100644
--- a/Sources/Orders/OrdersServiceCustom.swift
+++ b/Sources/Orders/OrdersServiceCustom.swift
@@ -1,10 +1,3 @@
-//
-//  OrdersServiceCustom.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 01/07/24.
-//
-
 import APNS
 import APNSCore
 import Fluent
@@ -18,14 +11,14 @@ import Zip
 /// Class to handle ``OrdersService``.
 ///
 /// The generics should be passed in this order:
+/// - Order Data Model
 /// - Order Type
 /// - Device Type
 /// - Registration Type
 /// - Error Log Type
-public final class OrdersServiceCustom<O, D, R: OrdersRegistrationModel, E: ErrorLogModel>: Sendable
-where O == R.OrderType, D == R.DeviceType {
+public final class OrdersServiceCustom<OD: OrderDataModel, O, D, R: OrdersRegistrationModel, E: ErrorLogModel>: Sendable
+where O == OD.OrderType, O == R.OrderType, D == R.DeviceType {
     private unowned let app: Application
-    private unowned let delegate: any OrdersDelegate
     private let logger: Logger?
 
     private let pemWWDRCertificate: String
@@ -40,7 +33,6 @@ where O == R.OrderType, D == R.DeviceType {
     ///
     /// - Parameters:
     ///   - app: The `Vapor.Application` to use in route handlers and APNs.
-    ///   - delegate: The ``OrdersDelegate`` to use for order generation.
     ///   - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
     ///   - logger: The `Logger` to use.
     ///   - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format.
@@ -50,7 +42,6 @@ where O == R.OrderType, D == R.DeviceType {
     ///   - openSSLPath: The location of the `openssl` command as a file path.
     public init(
         app: Application,
-        delegate: any OrdersDelegate,
         pushRoutesMiddleware: (any Middleware)? = nil,
         logger: Logger? = nil,
         pemWWDRCertificate: String,
@@ -60,7 +51,6 @@ where O == R.OrderType, D == R.DeviceType {
         openSSLPath: String = "/usr/bin/openssl"
     ) throws {
         self.app = app
-        self.delegate = delegate
         self.logger = logger
 
         self.pemWWDRCertificate = pemWWDRCertificate
@@ -102,25 +92,26 @@ where O == R.OrderType, D == R.DeviceType {
             isDefault: false
         )
 
+        let orderTypeIdentifier = PathComponent(stringLiteral: OD.typeIdentifier)
         let v1 = app.grouped("api", "orders", "v1")
-        v1.get("devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", use: { try await self.ordersForDevice(req: $0) })
+        v1.get("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, use: { try await self.ordersForDevice(req: $0) })
         v1.post("log", use: { try await self.logError(req: $0) })
 
         let v1auth = v1.grouped(AppleOrderMiddleware<O>())
         v1auth.post(
-            "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", ":orderIdentifier",
+            "devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, ":orderIdentifier",
             use: { try await self.registerDevice(req: $0) }
         )
-        v1auth.get("orders", ":orderTypeIdentifier", ":orderIdentifier", use: { try await self.latestVersionOfOrder(req: $0) })
+        v1auth.get("orders", orderTypeIdentifier, ":orderIdentifier", use: { try await self.latestVersionOfOrder(req: $0) })
         v1auth.delete(
-            "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", ":orderIdentifier",
+            "devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, ":orderIdentifier",
             use: { try await self.unregisterDevice(req: $0) }
         )
 
         if let pushRoutesMiddleware {
             let pushAuth = v1.grouped(pushRoutesMiddleware)
-            pushAuth.post("push", ":orderTypeIdentifier", ":orderIdentifier", use: { try await self.pushUpdatesForOrder(req: $0) })
-            pushAuth.get("push", ":orderTypeIdentifier", ":orderIdentifier", use: { try await self.tokensForOrderUpdate(req: $0) })
+            pushAuth.post("push", orderTypeIdentifier, ":orderIdentifier", use: { try await self.pushUpdatesForOrder(req: $0) })
+            pushAuth.get("push", orderTypeIdentifier, ":orderIdentifier", use: { try await self.tokensForOrderUpdate(req: $0) })
         }
     }
 }
@@ -135,15 +126,13 @@ extension OrdersServiceCustom {
             ifModifiedSince = ims
         }
 
-        guard let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier"),
-            let id = req.parameters.get("orderIdentifier", as: UUID.self)
-        else {
+        guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else {
             throw Abort(.badRequest)
         }
         guard
             let order = try await O.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == orderTypeIdentifier)
+                .filter(\._$typeIdentifier == OD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -153,6 +142,14 @@ extension OrdersServiceCustom {
             throw Abort(.notModified)
         }
 
+        guard
+            let orderData = try await OD.query(on: req.db)
+                .filter(\._$order.$id == id)
+                .first()
+        else {
+            throw Abort(.notFound)
+        }
+
         var headers = HTTPHeaders()
         headers.add(name: .contentType, value: "application/vnd.apple.order")
         headers.lastModified = HTTPHeaders.LastModified(order.updatedAt ?? Date.distantPast)
@@ -160,7 +157,7 @@ extension OrdersServiceCustom {
         return try await Response(
             status: .ok,
             headers: headers,
-            body: Response.Body(data: self.build(order: order, on: req.db))
+            body: Response.Body(data: self.build(order: orderData, on: req.db))
         )
     }
 
@@ -177,12 +174,11 @@ extension OrdersServiceCustom {
         guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")!
         let deviceIdentifier = req.parameters.get("deviceIdentifier")!
         guard
             let order = try await O.query(on: req.db)
                 .filter(\._$id == orderIdentifier)
-                .filter(\._$typeIdentifier == orderTypeIdentifier)
+                .filter(\._$typeIdentifier == OD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -224,12 +220,11 @@ extension OrdersServiceCustom {
     fileprivate func ordersForDevice(req: Request) async throws -> OrdersForDeviceDTO {
         logger?.debug("Called ordersForDevice")
 
-        let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")!
         let deviceIdentifier = req.parameters.get("deviceIdentifier")!
 
         var query = R.for(
             deviceLibraryIdentifier: deviceIdentifier,
-            typeIdentifier: orderTypeIdentifier,
+            typeIdentifier: OD.typeIdentifier,
             on: req.db
         )
         if let since: TimeInterval = req.query["ordersModifiedSince"] {
@@ -279,13 +274,12 @@ extension OrdersServiceCustom {
         guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")!
         let deviceIdentifier = req.parameters.get("deviceIdentifier")!
 
         guard
             let r = try await R.for(
                 deviceLibraryIdentifier: deviceIdentifier,
-                typeIdentifier: orderTypeIdentifier,
+                typeIdentifier: OD.typeIdentifier,
                 on: req.db
             )
             .filter(O.self, \._$id == orderIdentifier)
@@ -304,12 +298,11 @@ extension OrdersServiceCustom {
         guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")!
 
         guard
             let order = try await O.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == orderTypeIdentifier)
+                .filter(\._$typeIdentifier == OD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -325,12 +318,11 @@ extension OrdersServiceCustom {
         guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")!
 
         guard
             let order = try await O.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == orderTypeIdentifier)
+                .filter(\._$typeIdentifier == OD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -345,9 +337,13 @@ extension OrdersServiceCustom {
     /// Sends push notifications for a given order.
     ///
     /// - Parameters:
-    ///   - order: The order to send the notifications for.
+    ///   - orderData: The order to send the notifications for.
     ///   - db: The `Database` to use.
-    public func sendPushNotifications(for order: O, on db: any Database) async throws {
+    public func sendPushNotifications(for orderData: OD, on db: any Database) async throws {
+        try await sendPushNotifications(for: orderData._$order.get(on: db), on: db)
+    }
+
+    private func sendPushNotifications(for order: O, on db: any Database) async throws {
         let registrations = try await Self.registrations(for: order, on: db)
         for reg in registrations {
             let backgroundNotification = APNSBackgroundNotification(
@@ -375,7 +371,7 @@ extension OrdersServiceCustom {
             .join(parent: \._$device)
             .with(\._$order)
             .with(\._$device)
-            .filter(O.self, \._$typeIdentifier == order._$typeIdentifier.value!)
+            .filter(O.self, \._$typeIdentifier == OD.typeIdentifier)
             .filter(O.self, \._$id == order.requireID())
             .all()
     }
@@ -455,8 +451,8 @@ extension OrdersServiceCustom {
     ///   - db: The `Database` to use.
     ///
     /// - Returns: The generated order content as `Data`.
-    public func build(order: O, on db: any Database) async throws -> Data {
-        let filesDirectory = try await URL(fileURLWithPath: delegate.template(for: order, db: db), isDirectory: true)
+    public func build(order: OD, on db: any Database) async throws -> Data {
+        let filesDirectory = try await URL(fileURLWithPath: order.template(on: db), isDirectory: true)
         guard
             (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
         else {
@@ -469,7 +465,7 @@ extension OrdersServiceCustom {
 
         var files: [ArchiveFile] = []
 
-        let orderJSON = try await self.delegate.encode(order: order, db: db, encoder: self.encoder)
+        let orderJSON = try await self.encoder.encode(order.orderJSON(on: db))
         try orderJSON.write(to: tempDir.appendingPathComponent("order.json"))
         files.append(ArchiveFile(filename: "order.json", data: orderJSON))
 
diff --git a/Sources/PassKit/Models/DeviceModel.swift b/Sources/PassKit/Models/DeviceModel.swift
index 6062737..23c13ce 100644
--- a/Sources/PassKit/Models/DeviceModel.swift
+++ b/Sources/PassKit/Models/DeviceModel.swift
@@ -28,12 +28,12 @@
 
 import FluentKit
 
-/// Represents the `Model` that stores PassKit devices.
+/// Represents the `Model` that stores Apple Wallet devices.
 public protocol DeviceModel: Model where IDValue == Int {
     /// The push token used for sending updates to the device.
     var pushToken: String { get set }
 
-    /// The identifier PassKit provides for the device.
+    /// The identifier Apple Wallet provides for the device.
     var libraryIdentifier: String { get set }
 
     /// The designated initializer.
diff --git a/Sources/PassKit/Models/ErrorLogModel.swift b/Sources/PassKit/Models/ErrorLogModel.swift
index f49de87..3153c64 100644
--- a/Sources/PassKit/Models/ErrorLogModel.swift
+++ b/Sources/PassKit/Models/ErrorLogModel.swift
@@ -28,9 +28,9 @@
 
 import FluentKit
 
-/// Represents the `Model` that stores PassKit error logs.
+/// Represents the `Model` that stores Apple Wallet error logs.
 public protocol ErrorLogModel: Model {
-    /// The error message provided by PassKit.
+    /// The error message provided by Apple Wallet.
     var message: String { get set }
 
     /// The designated initializer.
diff --git a/Sources/PassKit/WalletError.swift b/Sources/PassKit/WalletError.swift
index 6209a7a..db48d6e 100644
--- a/Sources/PassKit/WalletError.swift
+++ b/Sources/PassKit/WalletError.swift
@@ -1,10 +1,3 @@
-//
-//  WalletError.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 04/07/24.
-//
-
 /// Errors that can be thrown by Apple Wallet passes and orders.
 public struct WalletError: Error, Sendable, Equatable {
     /// The type of the errors that can be thrown by Apple Wallet passes and orders.
diff --git a/Sources/Passes/DTOs/PassJSON.swift b/Sources/Passes/DTOs/PassJSON.swift
index c93ecb7..509fa89 100644
--- a/Sources/Passes/DTOs/PassJSON.swift
+++ b/Sources/Passes/DTOs/PassJSON.swift
@@ -1,10 +1,3 @@
-//
-//  PassJSON.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 28/06/24.
-//
-
 /// The structure of a `pass.json` file.
 public struct PassJSON {
     /// A protocol that defines the structure of a `pass.json` file.
diff --git a/Sources/Passes/DTOs/PersonalizationDictionaryDTO.swift b/Sources/Passes/DTOs/PersonalizationDictionaryDTO.swift
index 7ce0988..37b6302 100644
--- a/Sources/Passes/DTOs/PersonalizationDictionaryDTO.swift
+++ b/Sources/Passes/DTOs/PersonalizationDictionaryDTO.swift
@@ -1,10 +1,3 @@
-//
-//  PersonalizationDictionaryDTO.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 04/07/24.
-//
-
 import Vapor
 
 struct PersonalizationDictionaryDTO: Content {
diff --git a/Sources/Passes/DTOs/PersonalizationJSON.swift b/Sources/Passes/DTOs/PersonalizationJSON.swift
index 08fd2a5..17d527a 100644
--- a/Sources/Passes/DTOs/PersonalizationJSON.swift
+++ b/Sources/Passes/DTOs/PersonalizationJSON.swift
@@ -8,12 +8,12 @@ public struct PersonalizationJSON: Codable, Sendable {
     /// The contents of this array define the data requested from the user.
     ///
     /// The signup form’s fields are generated based on these keys.
-    var requiredPersonalizationFields: [PersonalizationField]
+    let requiredPersonalizationFields: [PersonalizationField]
 
     /// A brief description of the program.
     ///
     /// This is displayed on the signup sheet, under the personalization logo.
-    var description: String
+    let description: String
 
     /// A description of the program’s terms and conditions.
     ///
@@ -21,7 +21,7 @@ public struct PersonalizationJSON: Codable, Sendable {
     ///
     /// If present, this information is displayed after the user enters their personal information and taps the Next button.
     /// The user then has the option to agree to the terms, or to cancel out of the signup process.
-    var termsAndConditions: String?
+    let termsAndConditions: String?
 
     /// Initializes a new ``PersonalizationJSON`` instance.
     ///
diff --git a/Sources/Passes/Middleware/PassesService+AsyncModelMiddleware.swift b/Sources/Passes/Middleware/PassesService+AsyncModelMiddleware.swift
new file mode 100644
index 0000000..23c37e3
--- /dev/null
+++ b/Sources/Passes/Middleware/PassesService+AsyncModelMiddleware.swift
@@ -0,0 +1,42 @@
+import FluentKit
+import Foundation
+
+extension PassesService: AsyncModelMiddleware {
+    public func create(model: PD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let pass = Pass(
+            typeIdentifier: PD.typeIdentifier,
+            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
+        )
+        try await pass.save(on: db)
+        model._$pass.id = try pass.requireID()
+        try await next.create(model, on: db)
+    }
+
+    public func update(model: PD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let pass = try await model._$pass.get(on: db)
+        pass.updatedAt = Date.now
+        try await pass.save(on: db)
+        try await next.update(model, on: db)
+        try await self.sendPushNotifications(for: model, on: db)
+    }
+}
+
+extension PassesServiceCustom: AsyncModelMiddleware {
+    public func create(model: PD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let pass = P(
+            typeIdentifier: PD.typeIdentifier,
+            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
+        )
+        try await pass.save(on: db)
+        model._$pass.id = try pass.requireID()
+        try await next.create(model, on: db)
+    }
+
+    public func update(model: PD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
+        let pass = try await model._$pass.get(on: db)
+        pass.updatedAt = Date.now
+        try await pass.save(on: db)
+        try await next.update(model, on: db)
+        try await self.sendPushNotifications(for: model, on: db)
+    }
+}
diff --git a/Sources/Passes/Models/Concrete Models/Pass.swift b/Sources/Passes/Models/Concrete Models/Pass.swift
index e723208..33868c3 100644
--- a/Sources/Passes/Models/Concrete Models/Pass.swift	
+++ b/Sources/Passes/Models/Concrete Models/Pass.swift	
@@ -1,14 +1,7 @@
-//
-//  Pass.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 29/06/24.
-//
-
 import FluentKit
 import Foundation
 
-/// The `Model` that stores PassKit passes.
+/// The `Model` that stores Apple Wallet passes.
 ///
 /// Uses a UUID so people can't easily guess pass serial numbers.
 final public class Pass: PassModel, @unchecked Sendable {
diff --git a/Sources/Passes/Models/Concrete Models/PassesDevice.swift b/Sources/Passes/Models/Concrete Models/PassesDevice.swift
index 593b62f..a6eb292 100644
--- a/Sources/Passes/Models/Concrete Models/PassesDevice.swift	
+++ b/Sources/Passes/Models/Concrete Models/PassesDevice.swift	
@@ -1,14 +1,7 @@
-//
-//  PassesDevice.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 29/06/24.
-//
-
 import FluentKit
 import PassKit
 
-/// The `Model` that stores PassKit passes devices.
+/// The `Model` that stores Apple Wallet passes devices.
 final public class PassesDevice: DeviceModel, @unchecked Sendable {
     /// The schema name of the device model.
     public static let schema = PassesDevice.FieldKeys.schemaName
@@ -20,7 +13,7 @@ final public class PassesDevice: DeviceModel, @unchecked Sendable {
     @Field(key: PassesDevice.FieldKeys.pushToken)
     public var pushToken: String
 
-    /// The identifier PassKit provides for the device.
+    /// The identifier Apple Wallet provides for the device.
     @Field(key: PassesDevice.FieldKeys.libraryIdentifier)
     public var libraryIdentifier: String
 
diff --git a/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift b/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift
index 92b8c5f..11d6102 100644
--- a/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift	
+++ b/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift	
@@ -1,16 +1,9 @@
-//
-//  PassesErrorLog.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 29/06/24.
-//
-
 import FluentKit
 import PassKit
 
 import struct Foundation.Date
 
-/// The `Model` that stores PassKit passes error logs.
+/// The `Model` that stores Apple Wallet passes error logs.
 final public class PassesErrorLog: ErrorLogModel, @unchecked Sendable {
     /// The schema name of the error log model.
     public static let schema = PassesErrorLog.FieldKeys.schemaName
@@ -22,7 +15,7 @@ final public class PassesErrorLog: ErrorLogModel, @unchecked Sendable {
     @Timestamp(key: PassesErrorLog.FieldKeys.createdAt, on: .create)
     public var createdAt: Date?
 
-    /// The error message provided by PassKit.
+    /// The error message provided by Apple Wallet.
     @Field(key: PassesErrorLog.FieldKeys.message)
     public var message: String
 
diff --git a/Sources/Passes/Models/Concrete Models/PassesRegistration.swift b/Sources/Passes/Models/Concrete Models/PassesRegistration.swift
index 4ef396f..52ab07d 100644
--- a/Sources/Passes/Models/Concrete Models/PassesRegistration.swift	
+++ b/Sources/Passes/Models/Concrete Models/PassesRegistration.swift	
@@ -1,10 +1,3 @@
-//
-//  PassesRegistration.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 29/06/24.
-//
-
 import FluentKit
 
 /// The `Model` that stores passes registrations.
diff --git a/Sources/Passes/Models/Concrete Models/UserPersonalization.swift b/Sources/Passes/Models/Concrete Models/UserPersonalization.swift
index 4439fdb..e2c1688 100644
--- a/Sources/Passes/Models/Concrete Models/UserPersonalization.swift	
+++ b/Sources/Passes/Models/Concrete Models/UserPersonalization.swift	
@@ -1,10 +1,3 @@
-//
-//  UserPersonalization.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 12/07/24.
-//
-
 import FluentKit
 
 /// The `Model` that stores user personalization info.
diff --git a/Sources/Passes/Models/PassDataModel.swift b/Sources/Passes/Models/PassDataModel.swift
index 1c5b96b..47d1e0f 100644
--- a/Sources/Passes/Models/PassDataModel.swift
+++ b/Sources/Passes/Models/PassDataModel.swift
@@ -28,12 +28,53 @@
 
 import FluentKit
 
-/// Represents the `Model` that stores custom app data associated to PassKit passes.
+/// Represents the `Model` that stores custom app data associated to Apple Wallet passes.
 public protocol PassDataModel: Model {
     associatedtype PassType: PassModel
 
+    /// The pass type identifier that’s registered with Apple.
+    static var typeIdentifier: String { get }
+
     /// The foreign key to the pass table.
     var pass: PassType { get set }
+
+    /// Encode the pass into JSON.
+    ///
+    /// This method should generate the entire pass JSON.
+    ///
+    /// - Parameter db: The SQL database to query against.
+    ///
+    /// - Returns: An object that conforms to ``PassJSON/Properties``.
+    ///
+    /// > Tip: See the [`Pass`](https://developer.apple.com/documentation/walletpasses/pass) object to understand the keys.
+    func passJSON(on db: any Database) async throws -> any PassJSON.Properties
+
+    /// Should return a URL path which points to the template data for the pass.
+    ///
+    /// The path should point to a directory containing all the images and localizations for the generated `.pkpass` archive
+    /// but should *not* contain any of these items:
+    ///  - `manifest.json`
+    ///  - `pass.json`
+    ///  - `personalization.json`
+    ///  - `signature`
+    ///
+    /// - Parameter db: The SQL database to query against.
+    ///
+    /// - Returns: A URL path which points to the template data for the pass.
+    func template(on db: any Database) async throws -> String
+
+    /// Create the personalization JSON struct.
+    ///
+    /// This method should generate the entire personalization JSON struct.
+    /// If the pass in question requires personalization, you should return a ``PersonalizationJSON``.
+    /// If the pass does not require personalization, you should return `nil`.
+    ///
+    /// The default implementation of this method returns `nil`.
+    ///
+    /// - Parameter db: The SQL database to query against.
+    ///
+    /// - Returns: A ``PersonalizationJSON`` or `nil` if the pass does not require personalization.
+    func personalizationJSON(on db: any Database) async throws -> PersonalizationJSON?
 }
 
 extension PassDataModel {
@@ -47,3 +88,7 @@ extension PassDataModel {
         return pass
     }
 }
+
+extension PassDataModel {
+    public func personalizationJSON(on db: any Database) async throws -> PersonalizationJSON? { nil }
+}
diff --git a/Sources/Passes/Models/PassModel.swift b/Sources/Passes/Models/PassModel.swift
index e4fe6a8..a17c312 100644
--- a/Sources/Passes/Models/PassModel.swift
+++ b/Sources/Passes/Models/PassModel.swift
@@ -29,7 +29,7 @@
 import FluentKit
 import Foundation
 
-/// Represents the `Model` that stores PassKit passes.
+/// Represents the `Model` that stores Apple Wallet passes.
 ///
 /// Uses a UUID so people can't easily guess pass serial numbers.
 public protocol PassModel: Model where IDValue == UUID {
diff --git a/Sources/Passes/Models/UserPersonalizationModel.swift b/Sources/Passes/Models/UserPersonalizationModel.swift
index ccaa357..030001c 100644
--- a/Sources/Passes/Models/UserPersonalizationModel.swift
+++ b/Sources/Passes/Models/UserPersonalizationModel.swift
@@ -1,10 +1,3 @@
-//
-//  UserPersonalizationModel.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 12/07/24.
-//
-
 import FluentKit
 
 /// Represents the `Model` that stores user personalization info.
diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md
index d56b243..a788f88 100644
--- a/Sources/Passes/Passes.docc/GettingStarted.md
+++ b/Sources/Passes/Passes.docc/GettingStarted.md
@@ -9,21 +9,23 @@ For all the other custom data needed to generate the pass, such as the barcodes,
 The pass data model will be used to generate the `pass.json` file contents.
 
 The pass you distribute to a user is a signed bundle that contains the `pass.json` file, images and optional localizations.
-The Passes framework provides the ``PassesService`` class that handles the creation of the pass JSON file and the signing of the pass bundle, using a ``PassesDelegate`` that you must implement.
+The Passes framework provides the ``PassesService`` class that handles the creation of the pass JSON file and the signing of the pass bundle.
 The ``PassesService`` class also provides methods to send push notifications to all devices registered when you update a pass, and all the routes that Apple Wallet uses to retrieve passes.
 
 ### Implement the Pass Data Model
 
-Your data model should contain all the fields that you store for your pass, as well as a foreign key to ``Pass``, the pass model offered by the Passes framework.
+Your data model should contain all the fields that you store for your pass, as well as a foreign key to ``Pass``, the pass model offered by the Passes framework, and a pass type identifier that's registered with Apple.
 
 ```swift
 import Fluent
-import struct Foundation.UUID
+import Foundation
 import Passes
 
 final class PassData: PassDataModel, @unchecked Sendable {
     static let schema = "pass_data"
 
+    static let typeIdentifier = Environment.get("PASS_TYPE_IDENTIFIER")!
+
     @ID
     var id: UUID?
 
@@ -58,6 +60,23 @@ struct CreatePassData: AsyncMigration {
 }
 ```
 
+You also have to define two methods in the ``PassDataModel``:
+- ``PassDataModel/passJSON(on:)``, where you'll have to return a `struct` that conforms to ``PassJSON/Properties``.
+- ``PassDataModel/template(on:)``, where you'll have to return the path to a folder containing the pass files.
+
+```swift
+extension PassData {
+    func passJSON(on db: any Database) async throws -> any PassJSON.Properties {
+        try await PassJSONData(data: self, pass: self.$pass.get(on: db))
+    }
+
+    func template(on db: any Database) async throws -> String {
+        // The location might vary depending on the type of pass.
+        "Templates/Passes/"
+    }
+}
+```
+
 ### Handle Cleanup
 
 Depending on your implementation details, you may want to automatically clean out the passes and devices table when a registration is deleted.
@@ -79,7 +98,7 @@ struct PassJSONData: PassJSON.Properties {
     let description: String
     let formatVersion = PassJSON.FormatVersion.v1
     let organizationName = "vapor-community"
-    let passTypeIdentifier = Environment.get("PASS_TYPE_IDENTIFIER")!
+    let passTypeIdentifier = PassData.typeIdentifier
     let serialNumber: String
     let teamIdentifier = Environment.get("APPLE_TEAM_IDENTIFIER")!
 
@@ -132,41 +151,6 @@ struct PassJSONData: PassJSON.Properties {
 
 > Important: You **must** add `api/passes/` to your `webServiceURL`, as shown in the example above.
 
-### Implement the Delegate
-
-Create a delegate class that implements ``PassesDelegate``.
-
-Because the files for your pass' template and the method of encoding might vary by pass type, you'll be provided the ``Pass`` for those methods.
-In the ``PassesDelegate/encode(pass:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``PassJSON``.
-
-```swift
-import Vapor
-import Fluent
-import Passes
-
-final class PassDelegate: PassesDelegate {
-    func encode<P: PassModel>(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data {
-        // The specific PassData class you use here may vary based on the `pass.typeIdentifier`
-        // if you have multiple different types of passes, and thus multiple types of pass data.
-        guard let passData = try await PassData.query(on: db)
-            .filter(\.$pass.$id == pass.requireID())
-            .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-        guard let data = try? encoder.encode(PassJSONData(data: passData, pass: pass)) else {
-            throw Abort(.internalServerError)
-        }
-        return data
-    }
-
-    func template<P: PassModel>(for pass: P, db: Database) async throws -> String {
-        // The location might vary depending on the type of pass.
-        "Templates/Passes/"
-    }
-}
-```
-
 ### Initialize the Service
 
 Next, initialize the ``PassesService`` inside the `configure.swift` file.
@@ -179,13 +163,10 @@ import Fluent
 import Vapor
 import Passes
 
-let passDelegate = PassDelegate()
-
 public func configure(_ app: Application) async throws {
     ...
-    let passesService = try PassesService(
+    let passesService = try PassesService<PassData>(
         app: app,
-        delegate: passDelegate,
         pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!,
         pemCertificate: Environment.get("PEM_CERTIFICATE")!,
         pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")!
@@ -193,8 +174,6 @@ public func configure(_ app: Application) async throws {
 }
 ```
 
-> Note: Notice how the ``PassesDelegate`` is created as a global variable. You need to ensure that the delegate doesn't go out of scope as soon as the `configure(_:)` method exits.
-
 If you wish to include routes specifically for sending push notifications to updated passes, you can also pass to the ``PassesService`` initializer whatever `Middleware` you want Vapor to use to authenticate the two routes. Doing so will add two routes, the first one sends notifications and the second one retrieves a list of push tokens which would be sent a notification.
 
 ```http
@@ -215,11 +194,10 @@ import Vapor
 import PassKit
 import Passes
 
-let passDelegate = PassDelegate()
-
 public func configure(_ app: Application) async throws {
     ...
     let passesService = try PassesServiceCustom<
+        PassData,
         MyPassType,
         MyUserPersonalizationType,
         MyDeviceType,
@@ -227,7 +205,6 @@ public func configure(_ app: Application) async throws {
         MyErrorLogType
     >(
         app: app,
-        delegate: passDelegate,
         pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!,
         pemCertificate: Environment.get("PEM_CERTIFICATE")!,
         pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")!
@@ -240,56 +217,25 @@ public func configure(_ app: Application) async throws {
 If you're using the default schemas provided by this framework, you can register the default models in your `configure(_:)` method:
 
 ```swift
-PassesService.register(migrations: app.migrations)
+PassesService<PassData>.register(migrations: app.migrations)
 ```
 
 > Important: Register the default models before the migration of your pass data model.
 
 ### Pass Data Model Middleware
 
-You'll want to create a model middleware to handle the creation and update of the pass data model.
-This middleware could be responsible for creating and linking a ``Pass`` to the pass data model, depending on your requirements.
-When your pass data changes, it should also update the ``Pass/updatedAt`` field of the ``Pass`` and send a push notification to all devices registered to that pass. 
-
-```swift
-import Vapor
-import Fluent
-import Passes
-
-struct PassDataMiddleware: AsyncModelMiddleware {
-    private unowned let service: PassesService
-
-    init(service: PassesService) {
-        self.service = service
-    }
+This framework provides a model middleware to handle the creation and update of the pass data model.
 
-    // Create the `Pass` and add it to the `PassData` automatically at creation
-    func create(model: PassData, on db: Database, next: AnyAsyncModelResponder) async throws {
-        let pass = Pass(
-            typeIdentifier: Environment.get("PASS_TYPE_IDENTIFIER")!,
-            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString())
-        try await pass.save(on: db)
-        model.$pass.id = try pass.requireID()
-        try await next.create(model, on: db)
-    }
-
-    func update(model: PassData, on db: Database, next: AnyAsyncModelResponder) async throws {
-        let pass = try await model.$pass.get(on: db)
-        pass.updatedAt = Date()
-        try await pass.save(on: db)
-        try await next.update(model, on: db)
-        try await service.sendPushNotifications(for: pass, on: db)
-    }
-}
-```
+When you create a ``PassDataModel`` object, it will automatically create a ``PassModel`` object with a random auth token and the correct type identifier and link it to the pass data model.
+When you update a pass data model, it will update the ``PassModel`` object and send a push notification to all devices registered to that pass.
 
-You could register it in the `configure.swift` file.
+You can register it like so (either with a ``PassesService`` or a ``PassesServiceCustom``):
 
 ```swift
-app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .psql)
+app.databases.middleware.use(passesService, on: .psql)
 ```
 
-> Important: Whenever your pass data changes, you must update the ``Pass/updatedAt`` time of the linked ``Pass`` so that Wallet knows to retrieve a new pass.
+> Note: If you don't like the default implementation of the model middleware, it is highly recommended that you create your own. But remember: whenever your pass data changes, you must update the ``Pass/updatedAt`` time of the linked ``Pass`` so that Wallet knows to retrieve a new pass.
 
 ### Generate the Pass Content
 
@@ -316,15 +262,14 @@ Then use the object inside your route handlers to generate the pass bundle with
 ```swift
 fileprivate func passHandler(_ req: Request) async throws -> Response {
     ...
-    guard let passData = try await PassData.query(on: req.db)
+    guard let pass = try await PassData.query(on: req.db)
         .filter(...)
-        .with(\.$pass)
         .first()
     else {
         throw Abort(.notFound)
     }
 
-    let bundle = try await passesService.build(pass: passData.pass, on: req.db)
+    let bundle = try await passesService.build(pass: pass, on: req.db)
     let body = Response.Body(data: bundle)
     var headers = HTTPHeaders()
     headers.add(name: .contentType, value: "application/vnd.apple.pkpass")
@@ -346,8 +291,7 @@ The MIME type for a bundle of passes is "`application/vnd.apple.pkpasses`".
 ```swift
 fileprivate func passesHandler(_ req: Request) async throws -> Response {
     ...
-    let passesData = try await PassData.query(on: req.db).with(\.$pass).all()
-    let passes = passesData.map { $0.pass }
+    let passes = try await PassData.query(on: req.db).all()
 
     let bundle = try await passesService.build(passes: passes, on: req.db)
     let body = Response.Body(data: bundle)
diff --git a/Sources/Passes/Passes.docc/Passes.md b/Sources/Passes/Passes.docc/Passes.md
index 3d3ed98..4f962f0 100644
--- a/Sources/Passes/Passes.docc/Passes.md
+++ b/Sources/Passes/Passes.docc/Passes.md
@@ -22,11 +22,11 @@ For information on Apple Wallet passes, see the [Apple Developer Documentation](
 ### Essentials
 
 - <doc:GettingStarted>
+- ``PassDataModel``
 - ``PassJSON``
 
 ### Building and Distribution
 
-- ``PassesDelegate``
 - ``PassesService``
 - ``PassesServiceCustom``
 
@@ -41,7 +41,6 @@ For information on Apple Wallet passes, see the [Apple Developer Documentation](
 
 - ``PassModel``
 - ``PassesRegistrationModel``
-- ``PassDataModel``
 
 ### Personalized Passes (⚠️ WIP)
 
diff --git a/Sources/Passes/Passes.docc/Personalization.md b/Sources/Passes/Passes.docc/Personalization.md
index 7756781..c3814ae 100644
--- a/Sources/Passes/Passes.docc/Personalization.md
+++ b/Sources/Passes/Passes.docc/Personalization.md
@@ -12,52 +12,31 @@ Pass Personalization lets you create passes, referred to as personalizable passe
 
 Personalizable passes can be distributed like any other pass. For information on personalizable passes, see the [Wallet Developer Guide](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/PassPersonalization.html#//apple_ref/doc/uid/TP40012195-CH12-SW2) and [Return a Personalized Pass](https://developer.apple.com/documentation/walletpasses/return_a_personalized_pass).
 
-### Implement the Delegate
+### Implement the Data Model
 
-You'll have to make a few changes to ``PassesDelegate`` to support personalizable passes.
+You'll have to make a few changes to your ``PassDataModel`` to support personalizable passes.
 
 A personalizable pass is just a standard pass package with the following additional files:
 
 - A `personalization.json` file.
 - A `personalizationLogo@XX.png` file.
 
-Implement the ``PassesDelegate/personalizationJSON(for:db:)`` method, which gives you the ``Pass`` to encode.
+Implement the ``PassDataModel/personalizationJSON(on:)`` method.
 If the pass requires personalization, and if it was not already personalized, create the ``PersonalizationJSON`` struct, which will contain all the fields for the generated `personalization.json` file, and return it, otherwise return `nil`.
 
-In the ``PassesDelegate/template(for:db:)`` method, you have to return two different directory paths, depending on whether the pass has to be personalized or not. If it does, the directory must contain the `personalizationLogo@XX.png` file.
+In the ``PassDataModel/template(on:)`` method, you have to return two different directory paths, depending on whether the pass has to be personalized or not. If it does, the directory must contain the `personalizationLogo@XX.png` file.
 
-Finally, you have to implement the ``PassesDelegate/encode(pass:db:encoder:)`` method as usual, but remember to use in the ``PassJSON`` initializer the user info that will be saved inside ``Pass/userPersonalization`` after the pass has been personalized.
+Finally, you have to implement the ``PassDataModel/passJSON(on:)`` method as usual, but remember to use in the ``PassJSON/Properties`` initializer the user info that will be saved inside ``Pass/userPersonalization`` after the pass has been personalized.
 
 ```swift
-import Vapor
-import Fluent
-import Passes
-
-final class PassDelegate: PassesDelegate {
-    func encode<P: PassModel>(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data {
-        // Here encode the pass JSON data as usual.
-        guard let passData = try await PassData.query(on: db)
-            .filter(\.$pass.$id == pass.requireID())
-            .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-        guard let data = try? encoder.encode(PassJSONData(data: passData, pass: pass)) else {
-            throw Abort(.internalServerError)
-        }
-        return data
+extension PassData {
+    func passJSON(on db: any Database) async throws -> any PassJSON.Properties {
+        // Here create the pass JSON data as usual.
+        try await PassJSONData(data: self, pass: self.$pass.get(on: db))
     }
 
-    func personalizationJSON<P: PassModel>(for pass: P, db: any Database) async throws -> PersonalizationJSON? {
-        guard let passData = try await PassData.query(on: db)
-            .filter(\.$pass.$id == pass.requireID())
-            .with(\.$pass)
-            .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-
-        if try await passData.pass.$userPersonalization.get(on: db) == nil {
+    func personalizationJSON(on db: any Database) async throws -> PersonalizationJSON? {
+        if try await self.$pass.get(on: db).$userPersonalization.get(on: db) == nil {
             // If the pass requires personalization, create the personalization JSON struct.
             return PersonalizationJSON(
                 requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber],
@@ -69,15 +48,8 @@ final class PassDelegate: PassesDelegate {
         }
     }
 
-    func template<P: PassModel>(for pass: P, db: Database) async throws -> String {
-        guard let passData = try await PassData.query(on: db)
-            .filter(\.$pass.$id == pass.requireID())
-            .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-
-        if passData.requiresPersonalization {
+    func template(on db: any Database) async throws -> String {
+        if self.requiresPersonalization {
             // If the pass requires personalization, return the URL path to the personalization template,
             // which must contain the `personalizationLogo@XX.png` file.
             return "Templates/Passes/Personalization/"
@@ -91,7 +63,7 @@ final class PassDelegate: PassesDelegate {
 
 ### Implement the Web Service
 
-After implementing the delegate methods, there is nothing else you have to do.
+After implementing the data model methods, there is nothing else you have to do.
 
 Initializing the ``PassesService`` will automatically set up the endpoints that Apple Wallet expects to exist on your server to handle pass personalization.
 
@@ -103,10 +75,10 @@ Wallet will then send the user personal information to your server, which will b
 Immediately after that, Wallet will request the updated pass.
 This updated pass will contain the user personalization data that was previously saved inside the ``Pass/userPersonalization`` field.
 
-> Important: This updated and personalized pass **must not** contain the `personalization.json` file, so make sure that the ``PassesDelegate/personalizationJSON(for:db:)`` method returns `nil` when the pass has already been personalized.
+> Important: This updated and personalized pass **must not** contain the `personalization.json` file, so make sure that the ``PassDataModel/personalizationJSON(on:)`` method returns `nil` when the pass has already been personalized.
 
 ## Topics
 
-### Delegate Method
+### Data Model Method
 
-- ``PassesDelegate/personalizationJSON(for:db:)``
+- ``PassDataModel/personalizationJSON(on:)``
diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift
deleted file mode 100644
index 7b9c46d..0000000
--- a/Sources/Passes/PassesDelegate.swift
+++ /dev/null
@@ -1,87 +0,0 @@
-/// Copyright 2020 Gargoyle Software, LLC
-///
-/// Permission is hereby granted, free of charge, to any person obtaining a copy
-/// of this software and associated documentation files (the "Software"), to deal
-/// in the Software without restriction, including without limitation the rights
-/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-/// copies of the Software, and to permit persons to whom the Software is
-/// furnished to do so, subject to the following conditions:
-///
-/// The above copyright notice and this permission notice shall be included in
-/// all copies or substantial portions of the Software.
-///
-/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
-/// distribute, sublicense, create a derivative work, and/or sell copies of the
-/// Software in any work that is designed, intended, or marketed for pedagogical or
-/// instructional purposes related to programming, coding, application development,
-/// or information technology.  Permission for such use, copying, modification,
-/// merger, publication, distribution, sublicensing, creation of derivative works,
-/// or sale is expressly withheld.
-///
-/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-/// THE SOFTWARE.
-
-import FluentKit
-import Foundation
-
-/// The delegate which is responsible for generating the pass files.
-public protocol PassesDelegate: AnyObject, Sendable {
-    /// Should return a URL path which points to the template data for the pass.
-    ///
-    /// The path should point to a directory containing all the images and localizations for the generated `.pkpass` archive
-    /// but should *not* contain any of these items:
-    ///  - `manifest.json`
-    ///  - `pass.json`
-    ///  - `personalization.json`
-    ///  - `signature`
-    ///
-    /// - Parameters:
-    ///   - pass: The pass data from the SQL server.
-    ///   - db: The SQL database to query against.
-    ///
-    /// - Returns: A URL path which points to the template data for the pass.
-    func template<P: PassModel>(for pass: P, db: any Database) async throws -> String
-
-    /// Encode the pass into JSON.
-    ///
-    /// This method should generate the entire pass JSON. You are provided with
-    /// the pass data from the SQL database and you should return a properly
-    /// formatted pass file encoding.
-    ///
-    /// - Parameters:
-    ///   - pass: The pass data from the SQL server
-    ///   - db: The SQL database to query against.
-    ///   - encoder: The `JSONEncoder` which you should use.
-    /// - Returns: The encoded pass JSON data.
-    ///
-    /// > Tip: See the [`Pass`](https://developer.apple.com/documentation/walletpasses/pass) object to understand the keys.
-    func encode<P: PassModel>(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data
-
-    /// Create the personalization JSON struct.
-    ///
-    /// This method of the ``PassesDelegate`` should generate the entire personalization JSON struct.
-    /// You are provided with the pass data from the SQL database and,
-    /// if the pass in question requires personalization,
-    /// you should return a ``PersonalizationJSON``.
-    ///
-    /// If the pass does not require personalization, you should return `nil`.
-    ///
-    /// The default implementation of this method returns `nil`.
-    ///
-    /// - Parameters:
-    ///   - pass: The pass data from the SQL server.
-    ///   - db: The SQL database to query against.
-    /// - Returns: A ``PersonalizationJSON`` or `nil` if the pass does not require personalization.
-    func personalizationJSON<P: PassModel>(for pass: P, db: any Database) async throws -> PersonalizationJSON?
-}
-
-extension PassesDelegate {
-    public func personalizationJSON<P: PassModel>(for pass: P, db: any Database) async throws -> PersonalizationJSON? {
-        return nil
-    }
-}
diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift
index d32f433..18bf6e2 100644
--- a/Sources/Passes/PassesService.swift
+++ b/Sources/Passes/PassesService.swift
@@ -29,15 +29,14 @@
 import FluentKit
 import Vapor
 
-/// The main class that handles PassKit passes.
-public final class PassesService: Sendable {
-    private let service: PassesServiceCustom<Pass, UserPersonalization, PassesDevice, PassesRegistration, PassesErrorLog>
+/// The main class that handles Apple Wallet passes.
+public final class PassesService<PD: PassDataModel>: Sendable where Pass == PD.PassType {
+    private let service: PassesServiceCustom<PD, Pass, UserPersonalization, PassesDevice, PassesRegistration, PassesErrorLog>
 
-    /// Initializes the service and registers all the routes required for PassKit to work.
+    /// Initializes the service and registers all the routes required for Apple Wallet to work.
     ///
     /// - Parameters:
     ///   - app: The `Vapor.Application` to use in route handlers and APNs.
-    ///   - delegate: The ``PassesDelegate`` to use for pass generation.
     ///   - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
     ///   - logger: The `Logger` to use.
     ///   - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format.
@@ -47,7 +46,6 @@ public final class PassesService: Sendable {
     ///   - openSSLPath: The location of the `openssl` command as a file path.
     public init(
         app: Application,
-        delegate: any PassesDelegate,
         pushRoutesMiddleware: (any Middleware)? = nil,
         logger: Logger? = nil,
         pemWWDRCertificate: String,
@@ -58,7 +56,6 @@ public final class PassesService: Sendable {
     ) throws {
         self.service = try .init(
             app: app,
-            delegate: delegate,
             pushRoutesMiddleware: pushRoutesMiddleware,
             logger: logger,
             pemWWDRCertificate: pemWWDRCertificate,
@@ -76,7 +73,7 @@ public final class PassesService: Sendable {
     ///   - db: The `Database` to use.
     ///
     /// - Returns: The generated pass content as `Data`.
-    public func build(pass: Pass, on db: any Database) async throws -> Data {
+    public func build(pass: PD, on db: any Database) async throws -> Data {
         try await service.build(pass: pass, on: db)
     }
 
@@ -91,11 +88,11 @@ public final class PassesService: Sendable {
     ///   - db: The `Database` to use.
     ///
     /// - Returns: The bundle of passes as `Data`.
-    public func build(passes: [Pass], on db: any Database) async throws -> Data {
+    public func build(passes: [PD], on db: any Database) async throws -> Data {
         try await service.build(passes: passes, on: db)
     }
 
-    /// Adds the migrations for PassKit passes models.
+    /// Adds the migrations for Apple Wallet passes models.
     ///
     /// - Parameter migrations: The `Migrations` object to add the migrations to.
     public static func register(migrations: Migrations) {
@@ -111,7 +108,7 @@ public final class PassesService: Sendable {
     /// - Parameters:
     ///   - pass: The pass to send the notifications for.
     ///   - db: The `Database` to use.
-    public func sendPushNotifications(for pass: Pass, on db: any Database) async throws {
+    public func sendPushNotifications(for pass: PD, on db: any Database) async throws {
         try await service.sendPushNotifications(for: pass, on: db)
     }
 }
diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift
index 6c6fb31..f454e5d 100644
--- a/Sources/Passes/PassesServiceCustom.swift
+++ b/Sources/Passes/PassesServiceCustom.swift
@@ -1,10 +1,3 @@
-//
-//  PassesServiceCustom.swift
-//  PassKit
-//
-//  Created by Francesco Paolo Severino on 29/06/24.
-//
-
 import APNS
 import APNSCore
 import Fluent
@@ -18,15 +11,15 @@ import Zip
 /// Class to handle ``PassesService``.
 ///
 /// The generics should be passed in this order:
+/// - Pass Data Model
 /// - Pass Type
 /// - User Personalization Type
 /// - Device Type
 /// - Registration Type
 /// - Error Log Type
-public final class PassesServiceCustom<P, U, D, R: PassesRegistrationModel, E: ErrorLogModel>: Sendable
-where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType {
+public final class PassesServiceCustom<PD: PassDataModel, P, U, D, R: PassesRegistrationModel, E: ErrorLogModel>: Sendable
+where P == PD.PassType, P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType {
     private unowned let app: Application
-    private unowned let delegate: any PassesDelegate
     private let logger: Logger?
 
     private let pemWWDRCertificate: String
@@ -37,11 +30,10 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType {
 
     private let encoder = JSONEncoder()
 
-    /// Initializes the service and registers all the routes required for PassKit to work.
+    /// Initializes the service and registers all the routes required for Apple Wallet to work.
     ///
     /// - Parameters:
     ///   - app: The `Vapor.Application` to use in route handlers and APNs.
-    ///   - delegate: The ``PassesDelegate`` to use for pass generation.
     ///   - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
     ///   - logger: The `Logger` to use.
     ///   - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format.
@@ -51,7 +43,6 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType {
     ///   - openSSLPath: The location of the `openssl` command as a file path.
     public init(
         app: Application,
-        delegate: any PassesDelegate,
         pushRoutesMiddleware: (any Middleware)? = nil,
         logger: Logger? = nil,
         pemWWDRCertificate: String,
@@ -61,7 +52,6 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType {
         openSSLPath: String = "/usr/bin/openssl"
     ) throws {
         self.app = app
-        self.delegate = delegate
         self.logger = logger
 
         self.pemWWDRCertificate = pemWWDRCertificate
@@ -103,29 +93,30 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType {
             isDefault: false
         )
 
+        let passTypeIdentifier = PathComponent(stringLiteral: PD.typeIdentifier)
         let v1 = app.grouped("api", "passes", "v1")
         v1.get(
-            "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier",
+            "devices", ":deviceLibraryIdentifier", "registrations", passTypeIdentifier,
             use: { try await self.passesForDevice(req: $0) }
         )
         v1.post("log", use: { try await self.logError(req: $0) })
-        v1.post("passes", ":passTypeIdentifier", ":passSerial", "personalize", use: { try await self.personalizedPass(req: $0) })
+        v1.post("passes", passTypeIdentifier, ":passSerial", "personalize", use: { try await self.personalizedPass(req: $0) })
 
         let v1auth = v1.grouped(ApplePassMiddleware<P>())
         v1auth.post(
-            "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", ":passSerial",
+            "devices", ":deviceLibraryIdentifier", "registrations", passTypeIdentifier, ":passSerial",
             use: { try await self.registerDevice(req: $0) }
         )
-        v1auth.get("passes", ":passTypeIdentifier", ":passSerial", use: { try await self.latestVersionOfPass(req: $0) })
+        v1auth.get("passes", passTypeIdentifier, ":passSerial", use: { try await self.latestVersionOfPass(req: $0) })
         v1auth.delete(
-            "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", ":passSerial",
+            "devices", ":deviceLibraryIdentifier", "registrations", passTypeIdentifier, ":passSerial",
             use: { try await self.unregisterDevice(req: $0) }
         )
 
         if let pushRoutesMiddleware {
             let pushAuth = v1.grouped(pushRoutesMiddleware)
-            pushAuth.post("push", ":passTypeIdentifier", ":passSerial", use: { try await self.pushUpdatesForPass(req: $0) })
-            pushAuth.get("push", ":passTypeIdentifier", ":passSerial", use: { try await self.tokensForPassUpdate(req: $0) })
+            pushAuth.post("push", passTypeIdentifier, ":passSerial", use: { try await self.pushUpdatesForPass(req: $0) })
+            pushAuth.get("push", passTypeIdentifier, ":passSerial", use: { try await self.tokensForPassUpdate(req: $0) })
         }
     }
 }
@@ -145,11 +136,10 @@ extension PassesServiceCustom {
         guard let serial = req.parameters.get("passSerial", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let passTypeIdentifier = req.parameters.get("passTypeIdentifier")!
         let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")!
         guard
             let pass = try await P.query(on: req.db)
-                .filter(\._$typeIdentifier == passTypeIdentifier)
+                .filter(\._$typeIdentifier == PD.typeIdentifier)
                 .filter(\._$id == serial)
                 .first()
         else {
@@ -190,12 +180,11 @@ extension PassesServiceCustom {
     fileprivate func passesForDevice(req: Request) async throws -> PassesForDeviceDTO {
         logger?.debug("Called passesForDevice")
 
-        let passTypeIdentifier = req.parameters.get("passTypeIdentifier")!
         let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")!
 
         var query = R.for(
             deviceLibraryIdentifier: deviceLibraryIdentifier,
-            typeIdentifier: passTypeIdentifier,
+            typeIdentifier: PD.typeIdentifier,
             on: req.db
         )
         if let since: TimeInterval = req.query["passesUpdatedSince"] {
@@ -229,15 +218,13 @@ extension PassesServiceCustom {
             ifModifiedSince = ims
         }
 
-        guard let passTypeIdentifier = req.parameters.get("passTypeIdentifier"),
-            let id = req.parameters.get("passSerial", as: UUID.self)
-        else {
+        guard let id = req.parameters.get("passSerial", as: UUID.self) else {
             throw Abort(.badRequest)
         }
         guard
             let pass = try await P.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == passTypeIdentifier)
+                .filter(\._$typeIdentifier == PD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -247,6 +234,14 @@ extension PassesServiceCustom {
             throw Abort(.notModified)
         }
 
+        guard
+            let passData = try await PD.query(on: req.db)
+                .filter(\._$pass.$id == id)
+                .first()
+        else {
+            throw Abort(.notFound)
+        }
+
         var headers = HTTPHeaders()
         headers.add(name: .contentType, value: "application/vnd.apple.pkpass")
         headers.lastModified = HTTPHeaders.LastModified(pass.updatedAt ?? Date.distantPast)
@@ -254,7 +249,7 @@ extension PassesServiceCustom {
         return try await Response(
             status: .ok,
             headers: headers,
-            body: Response.Body(data: self.build(pass: pass, on: req.db))
+            body: Response.Body(data: self.build(pass: passData, on: req.db))
         )
     }
 
@@ -264,13 +259,12 @@ extension PassesServiceCustom {
         guard let passId = req.parameters.get("passSerial", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let passTypeIdentifier = req.parameters.get("passTypeIdentifier")!
         let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")!
 
         guard
             let r = try await R.for(
                 deviceLibraryIdentifier: deviceLibraryIdentifier,
-                typeIdentifier: passTypeIdentifier,
+                typeIdentifier: PD.typeIdentifier,
                 on: req.db
             )
             .filter(P.self, \._$id == passId)
@@ -303,15 +297,13 @@ extension PassesServiceCustom {
     fileprivate func personalizedPass(req: Request) async throws -> Response {
         logger?.debug("Called personalizedPass")
 
-        guard let passTypeIdentifier = req.parameters.get("passTypeIdentifier"),
-            let id = req.parameters.get("passSerial", as: UUID.self)
-        else {
+        guard let id = req.parameters.get("passSerial", as: UUID.self) else {
             throw Abort(.badRequest)
         }
         guard
             let pass = try await P.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == passTypeIdentifier)
+                .filter(\._$typeIdentifier == PD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -349,12 +341,11 @@ extension PassesServiceCustom {
         guard let id = req.parameters.get("passSerial", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let passTypeIdentifier = req.parameters.get("passTypeIdentifier")!
 
         guard
             let pass = try await P.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == passTypeIdentifier)
+                .filter(\._$typeIdentifier == PD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -370,12 +361,11 @@ extension PassesServiceCustom {
         guard let id = req.parameters.get("passSerial", as: UUID.self) else {
             throw Abort(.badRequest)
         }
-        let passTypeIdentifier = req.parameters.get("passTypeIdentifier")!
 
         guard
             let pass = try await P.query(on: req.db)
                 .filter(\._$id == id)
-                .filter(\._$typeIdentifier == passTypeIdentifier)
+                .filter(\._$typeIdentifier == PD.typeIdentifier)
                 .first()
         else {
             throw Abort(.notFound)
@@ -390,9 +380,13 @@ extension PassesServiceCustom {
     /// Sends push notifications for a given pass.
     ///
     /// - Parameters:
-    ///   - pass: The pass to send the notifications for.
+    ///   - passData: The pass to send the notifications for.
     ///   - db: The `Database` to use.
-    public func sendPushNotifications(for pass: P, on db: any Database) async throws {
+    public func sendPushNotifications(for passData: PD, on db: any Database) async throws {
+        try await self.sendPushNotifications(for: passData._$pass.get(on: db), on: db)
+    }
+
+    private func sendPushNotifications(for pass: P, on db: any Database) async throws {
         let registrations = try await Self.registrations(for: pass, on: db)
         for reg in registrations {
             let backgroundNotification = APNSBackgroundNotification(
@@ -420,7 +414,7 @@ extension PassesServiceCustom {
             .join(parent: \._$device)
             .with(\._$pass)
             .with(\._$device)
-            .filter(P.self, \._$typeIdentifier == pass._$typeIdentifier.value!)
+            .filter(P.self, \._$typeIdentifier == PD.typeIdentifier)
             .filter(P.self, \._$id == pass.requireID())
             .all()
     }
@@ -501,8 +495,8 @@ extension PassesServiceCustom {
     ///   - db: The `Database` to use.
     ///
     /// - Returns: The generated pass content as `Data`.
-    public func build(pass: P, on db: any Database) async throws -> Data {
-        let filesDirectory = try await URL(fileURLWithPath: delegate.template(for: pass, db: db), isDirectory: true)
+    public func build(pass: PD, on db: any Database) async throws -> Data {
+        let filesDirectory = try await URL(fileURLWithPath: pass.template(on: db), isDirectory: true)
         guard
             (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
         else {
@@ -515,12 +509,12 @@ extension PassesServiceCustom {
 
         var files: [ArchiveFile] = []
 
-        let passJSON = try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder)
+        let passJSON = try await self.encoder.encode(pass.passJSON(on: db))
         try passJSON.write(to: tempDir.appendingPathComponent("pass.json"))
         files.append(ArchiveFile(filename: "pass.json", data: passJSON))
 
         // Pass Personalization
-        if let personalizationJSON = try await self.delegate.personalizationJSON(for: pass, db: db) {
+        if let personalizationJSON = try await pass.personalizationJSON(on: db) {
             let personalizationJSONData = try self.encoder.encode(personalizationJSON)
             try personalizationJSONData.write(to: tempDir.appendingPathComponent("personalization.json"))
             files.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData))
@@ -556,7 +550,7 @@ extension PassesServiceCustom {
     ///   - db: The `Database` to use.
     ///
     /// - Returns: The bundle of passes as `Data`.
-    public func build(passes: [P], on db: any Database) async throws -> Data {
+    public func build(passes: [PD], on db: any Database) async throws -> Data {
         guard passes.count > 1 && passes.count <= 10 else {
             throw WalletError.invalidNumberOfPasses
         }
diff --git a/Tests/OrdersTests/OrderData.swift b/Tests/OrdersTests/OrderData.swift
deleted file mode 100644
index 627ecd3..0000000
--- a/Tests/OrdersTests/OrderData.swift
+++ /dev/null
@@ -1,122 +0,0 @@
-import Fluent
-import Orders
-import Vapor
-
-import struct Foundation.UUID
-
-final class OrderData: OrderDataModel, @unchecked Sendable {
-    static let schema = OrderData.FieldKeys.schemaName
-
-    @ID(key: .id)
-    var id: UUID?
-
-    @Field(key: OrderData.FieldKeys.title)
-    var title: String
-
-    @Parent(key: OrderData.FieldKeys.orderID)
-    var order: Order
-
-    init() {}
-
-    init(id: UUID? = nil, title: String) {
-        self.id = id
-        self.title = title
-    }
-}
-
-struct CreateOrderData: AsyncMigration {
-    func prepare(on database: any Database) async throws {
-        try await database.schema(OrderData.FieldKeys.schemaName)
-            .id()
-            .field(OrderData.FieldKeys.title, .string, .required)
-            .field(OrderData.FieldKeys.orderID, .uuid, .required, .references(Order.schema, .id, onDelete: .cascade))
-            .create()
-    }
-
-    func revert(on database: any Database) async throws {
-        try await database.schema(OrderData.FieldKeys.schemaName).delete()
-    }
-}
-
-extension OrderData {
-    enum FieldKeys {
-        static let schemaName = "order_data"
-        static let title = FieldKey(stringLiteral: "title")
-        static let orderID = FieldKey(stringLiteral: "order_id")
-    }
-}
-
-extension OrderJSON.SchemaVersion: Decodable {}
-extension OrderJSON.OrderType: Decodable {}
-extension OrderJSON.OrderStatus: Decodable {}
-
-struct OrderJSONData: OrderJSON.Properties, Decodable {
-    let schemaVersion = OrderJSON.SchemaVersion.v1
-    let orderTypeIdentifier = "order.com.example.pet-store"
-    let orderIdentifier: String
-    let orderType = OrderJSON.OrderType.ecommerce
-    let orderNumber = "HM090772020864"
-    let createdAt: String
-    let updatedAt: String
-    let status = OrderJSON.OrderStatus.open
-    let merchant: MerchantData
-    let orderManagementURL = "https://www.example.com/"
-    let authenticationToken: String
-
-    private let webServiceURL = "https://www.example.com/api/orders/"
-
-    enum CodingKeys: String, CodingKey {
-        case schemaVersion
-        case orderTypeIdentifier, orderIdentifier, orderType, orderNumber
-        case createdAt, updatedAt
-        case status, merchant
-        case orderManagementURL, authenticationToken, webServiceURL
-    }
-
-    struct MerchantData: OrderJSON.Merchant, Decodable {
-        let merchantIdentifier = "com.example.pet-store"
-        let displayName: String
-        let url = "https://www.example.com/"
-        let logo = "pet_store_logo.png"
-
-        enum CodingKeys: String, CodingKey {
-            case merchantIdentifier, displayName, url, logo
-        }
-    }
-
-    init(data: OrderData, order: Order) {
-        self.orderIdentifier = order.id!.uuidString
-        self.authenticationToken = order.authenticationToken
-        self.merchant = MerchantData(displayName: data.title)
-        let dateFormatter = ISO8601DateFormatter()
-        dateFormatter.formatOptions = .withInternetDateTime
-        self.createdAt = dateFormatter.string(from: order.createdAt!)
-        self.updatedAt = dateFormatter.string(from: order.updatedAt!)
-    }
-}
-
-struct OrderDataMiddleware: AsyncModelMiddleware {
-    private unowned let service: OrdersService
-
-    init(service: OrdersService) {
-        self.service = service
-    }
-
-    func create(model: OrderData, on db: any Database, next: any AnyAsyncModelResponder) async throws {
-        let order = Order(
-            typeIdentifier: "order.com.example.pet-store",
-            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
-        )
-        try await order.save(on: db)
-        model.$order.id = try order.requireID()
-        try await next.create(model, on: db)
-    }
-
-    func update(model: OrderData, on db: any Database, next: any AnyAsyncModelResponder) async throws {
-        let order = try await model.$order.get(on: db)
-        order.updatedAt = Date()
-        try await order.save(on: db)
-        try await next.update(model, on: db)
-        try await service.sendPushNotifications(for: order, on: db)
-    }
-}
diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift
index 4d41358..2fb7901 100644
--- a/Tests/OrdersTests/OrdersTests.swift
+++ b/Tests/OrdersTests/OrdersTests.swift
@@ -1,4 +1,3 @@
-import FluentKit
 import PassKit
 import Testing
 import XCTVapor
@@ -17,7 +16,9 @@ struct OrdersTests {
             let orderData = OrderData(title: "Test Order")
             try await orderData.create(on: app.db)
             let order = try await orderData.$order.get(on: app.db)
-            let data = try await ordersService.build(order: order, on: app.db)
+
+            let data = try await ordersService.build(order: orderData, on: app.db)
+
             let orderURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).order")
             try data.write(to: orderURL)
             let orderFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
@@ -327,7 +328,7 @@ struct OrdersTests {
             try await orderData.create(on: app.db)
             let order = try await orderData._$order.get(on: app.db)
 
-            try await ordersService.sendPushNotifications(for: order, on: app.db)
+            try await ordersService.sendPushNotifications(for: orderData, on: app.db)
 
             let deviceLibraryIdentifier = "abcdefg"
             let pushToken = "1234567890"
@@ -383,7 +384,7 @@ struct OrdersTests {
             )
 
             if !useEncryptedKey {
-                // Test `OrderDataMiddleware` update method
+                // Test `AsyncModelMiddleware` update method
                 orderData.title = "Test Order 2"
                 do {
                     try await orderData.update(on: app.db)
diff --git a/Tests/OrdersTests/TestOrdersDelegate.swift b/Tests/OrdersTests/TestOrdersDelegate.swift
deleted file mode 100644
index b5b45bb..0000000
--- a/Tests/OrdersTests/TestOrdersDelegate.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-import FluentKit
-import Orders
-import Vapor
-
-final class TestOrdersDelegate: OrdersDelegate {
-    func encode<O: OrderModel>(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data {
-        guard
-            let orderData = try await OrderData.query(on: db)
-                .filter(\.$order.$id == order.requireID())
-                .with(\.$order)
-                .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-        guard let data = try? encoder.encode(OrderJSONData(data: orderData, order: orderData.order)) else {
-            throw Abort(.internalServerError)
-        }
-        return data
-    }
-
-    func template<O: OrderModel>(for: O, db: any Database) async throws -> String {
-        "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/"
-    }
-}
diff --git a/Tests/OrdersTests/Utils/OrderData.swift b/Tests/OrdersTests/Utils/OrderData.swift
new file mode 100644
index 0000000..180d291
--- /dev/null
+++ b/Tests/OrdersTests/Utils/OrderData.swift
@@ -0,0 +1,57 @@
+import Fluent
+import Foundation
+import Orders
+
+final class OrderData: OrderDataModel, @unchecked Sendable {
+    static let schema = OrderData.FieldKeys.schemaName
+
+    static let typeIdentifier = "order.com.example.pet-store"
+
+    @ID(key: .id)
+    var id: UUID?
+
+    @Field(key: OrderData.FieldKeys.title)
+    var title: String
+
+    @Parent(key: OrderData.FieldKeys.orderID)
+    var order: Order
+
+    init() {}
+
+    init(id: UUID? = nil, title: String) {
+        self.id = id
+        self.title = title
+    }
+}
+
+struct CreateOrderData: AsyncMigration {
+    func prepare(on database: any Database) async throws {
+        try await database.schema(OrderData.FieldKeys.schemaName)
+            .id()
+            .field(OrderData.FieldKeys.title, .string, .required)
+            .field(OrderData.FieldKeys.orderID, .uuid, .required, .references(Order.schema, .id, onDelete: .cascade))
+            .create()
+    }
+
+    func revert(on database: any Database) async throws {
+        try await database.schema(OrderData.FieldKeys.schemaName).delete()
+    }
+}
+
+extension OrderData {
+    enum FieldKeys {
+        static let schemaName = "order_data"
+        static let title = FieldKey(stringLiteral: "title")
+        static let orderID = FieldKey(stringLiteral: "order_id")
+    }
+}
+
+extension OrderData {
+    func orderJSON(on db: any Database) async throws -> any OrderJSON.Properties {
+        try await OrderJSONData(data: self, order: self.$order.get(on: db))
+    }
+
+    func template(on db: any Database) async throws -> String {
+        "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/"
+    }
+}
diff --git a/Tests/OrdersTests/Utils/OrderJSONData.swift b/Tests/OrdersTests/Utils/OrderJSONData.swift
new file mode 100644
index 0000000..d486ceb
--- /dev/null
+++ b/Tests/OrdersTests/Utils/OrderJSONData.swift
@@ -0,0 +1,51 @@
+import Foundation
+import Orders
+
+extension OrderJSON.SchemaVersion: Decodable {}
+extension OrderJSON.OrderType: Decodable {}
+extension OrderJSON.OrderStatus: Decodable {}
+
+struct OrderJSONData: OrderJSON.Properties, Decodable {
+    let schemaVersion = OrderJSON.SchemaVersion.v1
+    let orderTypeIdentifier = OrderData.typeIdentifier
+    let orderIdentifier: String
+    let orderType = OrderJSON.OrderType.ecommerce
+    let orderNumber = "HM090772020864"
+    let createdAt: String
+    let updatedAt: String
+    let status = OrderJSON.OrderStatus.open
+    let merchant: MerchantData
+    let orderManagementURL = "https://www.example.com/"
+    let authenticationToken: String
+
+    private let webServiceURL = "https://www.example.com/api/orders/"
+
+    enum CodingKeys: String, CodingKey {
+        case schemaVersion
+        case orderTypeIdentifier, orderIdentifier, orderType, orderNumber
+        case createdAt, updatedAt
+        case status, merchant
+        case orderManagementURL, authenticationToken, webServiceURL
+    }
+
+    struct MerchantData: OrderJSON.Merchant, Decodable {
+        let merchantIdentifier = "com.example.pet-store"
+        let displayName: String
+        let url = "https://www.example.com/"
+        let logo = "pet_store_logo.png"
+
+        enum CodingKeys: String, CodingKey {
+            case merchantIdentifier, displayName, url, logo
+        }
+    }
+
+    init(data: OrderData, order: Order) {
+        self.orderIdentifier = order.id!.uuidString
+        self.authenticationToken = order.authenticationToken
+        self.merchant = MerchantData(displayName: data.title)
+        let dateFormatter = ISO8601DateFormatter()
+        dateFormatter.formatOptions = .withInternetDateTime
+        self.createdAt = dateFormatter.string(from: order.createdAt!)
+        self.updatedAt = dateFormatter.string(from: order.updatedAt!)
+    }
+}
diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/Utils/withApp.swift
similarity index 77%
rename from Tests/OrdersTests/withApp.swift
rename to Tests/OrdersTests/Utils/withApp.swift
index 8928800..a1c889a 100644
--- a/Tests/OrdersTests/withApp.swift
+++ b/Tests/OrdersTests/Utils/withApp.swift
@@ -8,21 +8,19 @@ import Zip
 
 func withApp(
     useEncryptedKey: Bool = false,
-    _ body: (Application, OrdersService) async throws -> Void
+    _ body: (Application, OrdersService<OrderData>) async throws -> Void
 ) async throws {
     let app = try await Application.make(.testing)
     do {
         try #require(isLoggingConfigured)
 
         app.databases.use(.sqlite(.memory), as: .sqlite)
-        OrdersService.register(migrations: app.migrations)
+        OrdersService<OrderData>.register(migrations: app.migrations)
         app.migrations.add(CreateOrderData())
         try await app.autoMigrate()
 
-        let delegate = TestOrdersDelegate()
-        let ordersService = try OrdersService(
+        let ordersService = try OrdersService<OrderData>(
             app: app,
-            delegate: delegate,
             pushRoutesMiddleware: SecretMiddleware(secret: "foo"),
             logger: app.logger,
             pemWWDRCertificate: TestCertificate.pemWWDRCertificate,
@@ -30,7 +28,7 @@ func withApp(
             pemPrivateKey: useEncryptedKey ? TestCertificate.encryptedPemPrivateKey : TestCertificate.pemPrivateKey,
             pemPrivateKeyPassword: useEncryptedKey ? "password" : nil
         )
-        app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite)
+        app.databases.middleware.use(ordersService, on: .sqlite)
 
         Zip.addCustomFileExtension("order")
 
@@ -38,6 +36,7 @@ func withApp(
 
         try await app.autoRevert()
     } catch {
+        try? await app.autoRevert()
         try await app.asyncShutdown()
         throw error
     }
diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift
index 9bc3637..3661783 100644
--- a/Tests/PassesTests/PassesTests.swift
+++ b/Tests/PassesTests/PassesTests.swift
@@ -1,4 +1,3 @@
-import FluentKit
 import PassKit
 import Testing
 import XCTVapor
@@ -17,7 +16,9 @@ struct PassesTests {
             let passData = PassData(title: "Test Pass")
             try await passData.create(on: app.db)
             let pass = try await passData.$pass.get(on: app.db)
-            let data = try await passesService.build(pass: pass, on: app.db)
+
+            let data = try await passesService.build(pass: passData, on: app.db)
+
             let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass")
             try data.write(to: passURL)
             let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
@@ -54,17 +55,15 @@ struct PassesTests {
         try await withApp { app, passesService in
             let passData1 = PassData(title: "Test Pass 1")
             try await passData1.create(on: app.db)
-            let pass1 = try await passData1.$pass.get(on: app.db)
 
             let passData2 = PassData(title: "Test Pass 2")
             try await passData2.create(on: app.db)
-            let pass2 = try await passData2._$pass.get(on: app.db)
 
-            let data = try await passesService.build(passes: [pass1, pass2], on: app.db)
+            let data = try await passesService.build(passes: [passData1, passData2], on: app.db)
             #expect(data != nil)
 
             do {
-                let data = try await passesService.build(passes: [pass1], on: app.db)
+                let data = try await passesService.build(passes: [passData1], on: app.db)
                 Issue.record("Expected error, got \(data)")
             } catch let error as WalletError {
                 #expect(error == .invalidNumberOfPasses)
@@ -78,7 +77,9 @@ struct PassesTests {
             let passData = PassData(title: "Personalize")
             try await passData.create(on: app.db)
             let pass = try await passData.$pass.get(on: app.db)
-            let data = try await passesService.build(pass: pass, on: app.db)
+
+            let data = try await passesService.build(pass: passData, on: app.db)
+
             let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass")
             try data.write(to: passURL)
             let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
@@ -463,9 +464,9 @@ struct PassesTests {
 
             let passData = PassData(title: "Test Pass")
             try await passData.create(on: app.db)
-            let pass = try await passData._$pass.get(on: app.db)
+            let pass = try await passData.$pass.get(on: app.db)
 
-            try await passesService.sendPushNotifications(for: pass, on: app.db)
+            try await passesService.sendPushNotifications(for: passData, on: app.db)
 
             let deviceLibraryIdentifier = "abcdefg"
             let pushToken = "1234567890"
@@ -521,7 +522,7 @@ struct PassesTests {
             )
 
             if !useEncryptedKey {
-                // Test `PassDataMiddleware` update method
+                // Test `AsyncModelMiddleware` update method
                 passData.title = "Test Pass 2"
                 do {
                     try await passData.update(on: app.db)
@@ -541,22 +542,4 @@ struct PassesTests {
         #expect(WalletError.noSourceFiles == WalletError.noSourceFiles)
         #expect(WalletError.noOpenSSLExecutable != WalletError.invalidNumberOfPasses)
     }
-
-    @Test("Default PassesDelegate Properties")
-    func defaultDelegate() async throws {
-        final class DefaultPassesDelegate: PassesDelegate {
-            func template<P: PassModel>(for pass: P, db: any Database) async throws -> String { "" }
-            func encode<P: PassModel>(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data { Data() }
-        }
-
-        let defaultDelegate = DefaultPassesDelegate()
-
-        try await withApp { app, passesService in
-            let passData = PassData(title: "Test Pass")
-            try await passData.create(on: app.db)
-            let pass = try await passData.$pass.get(on: app.db)
-            let data = try await defaultDelegate.personalizationJSON(for: pass, db: app.db)
-            #expect(data == nil)
-        }
-    }
 }
diff --git a/Tests/PassesTests/TestPassesDelegate.swift b/Tests/PassesTests/TestPassesDelegate.swift
deleted file mode 100644
index c323c8d..0000000
--- a/Tests/PassesTests/TestPassesDelegate.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-import FluentKit
-import Passes
-import Vapor
-
-final class TestPassesDelegate: PassesDelegate {
-    func encode<P: PassModel>(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data {
-        guard
-            let passData = try await PassData.query(on: db)
-                .filter(\.$pass.$id == pass.requireID())
-                .with(\.$pass)
-                .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-        guard let data = try? encoder.encode(PassJSONData(data: passData, pass: passData.pass)) else {
-            throw Abort(.internalServerError)
-        }
-        return data
-    }
-
-    func personalizationJSON<P: PassModel>(for pass: P, db: any Database) async throws -> PersonalizationJSON? {
-        guard
-            let passData = try await PassData.query(on: db)
-                .filter(\.$pass.$id == pass.id!)
-                .with(\.$pass)
-                .first()
-        else {
-            throw Abort(.internalServerError)
-        }
-
-        if passData.title != "Personalize" { return nil }
-
-        if try await passData.pass.$userPersonalization.get(on: db) == nil {
-            return PersonalizationJSON(
-                requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber],
-                description: "Hello, World!"
-            )
-        } else {
-            return nil
-        }
-    }
-
-    func template<P: PassModel>(for pass: P, db: any Database) async throws -> String {
-        "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/"
-    }
-}
diff --git a/Tests/PassesTests/Utils/PassData.swift b/Tests/PassesTests/Utils/PassData.swift
new file mode 100644
index 0000000..063d806
--- /dev/null
+++ b/Tests/PassesTests/Utils/PassData.swift
@@ -0,0 +1,70 @@
+import Fluent
+import Foundation
+import Passes
+
+final class PassData: PassDataModel, @unchecked Sendable {
+    static let schema = PassData.FieldKeys.schemaName
+
+    static let typeIdentifier = "pass.com.vapor-community.Passes"
+
+    @ID(key: .id)
+    var id: UUID?
+
+    @Field(key: PassData.FieldKeys.title)
+    var title: String
+
+    @Parent(key: PassData.FieldKeys.passID)
+    var pass: Pass
+
+    init() {}
+
+    init(id: UUID? = nil, title: String) {
+        self.id = id
+        self.title = title
+    }
+}
+
+struct CreatePassData: AsyncMigration {
+    func prepare(on database: any Database) async throws {
+        try await database.schema(PassData.FieldKeys.schemaName)
+            .id()
+            .field(PassData.FieldKeys.title, .string, .required)
+            .field(PassData.FieldKeys.passID, .uuid, .required, .references(Pass.schema, .id, onDelete: .cascade))
+            .create()
+    }
+
+    func revert(on database: any Database) async throws {
+        try await database.schema(PassData.FieldKeys.schemaName).delete()
+    }
+}
+
+extension PassData {
+    enum FieldKeys {
+        static let schemaName = "pass_data"
+        static let title = FieldKey(stringLiteral: "title")
+        static let passID = FieldKey(stringLiteral: "pass_id")
+    }
+}
+
+extension PassData {
+    func passJSON(on db: any Database) async throws -> any PassJSON.Properties {
+        try await PassJSONData(data: self, pass: self.$pass.get(on: db))
+    }
+
+    func template(on db: any Database) async throws -> String {
+        "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/"
+    }
+
+    func personalizationJSON(on db: any Database) async throws -> PersonalizationJSON? {
+        if self.title != "Personalize" { return nil }
+
+        if try await self.$pass.get(on: db).$userPersonalization.get(on: db) == nil {
+            return PersonalizationJSON(
+                requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber],
+                description: "Hello, World!"
+            )
+        } else {
+            return nil
+        }
+    }
+}
diff --git a/Tests/PassesTests/PassData.swift b/Tests/PassesTests/Utils/PassJSONData.swift
similarity index 54%
rename from Tests/PassesTests/PassData.swift
rename to Tests/PassesTests/Utils/PassJSONData.swift
index c5388c4..91f15ed 100644
--- a/Tests/PassesTests/PassData.swift
+++ b/Tests/PassesTests/Utils/PassJSONData.swift
@@ -1,50 +1,4 @@
-import Fluent
 import Passes
-import Vapor
-
-import struct Foundation.UUID
-
-final class PassData: PassDataModel, @unchecked Sendable {
-    static let schema = PassData.FieldKeys.schemaName
-
-    @ID(key: .id)
-    var id: UUID?
-
-    @Field(key: PassData.FieldKeys.title)
-    var title: String
-
-    @Parent(key: PassData.FieldKeys.passID)
-    var pass: Pass
-
-    init() {}
-
-    init(id: UUID? = nil, title: String) {
-        self.id = id
-        self.title = title
-    }
-}
-
-struct CreatePassData: AsyncMigration {
-    func prepare(on database: any Database) async throws {
-        try await database.schema(PassData.FieldKeys.schemaName)
-            .id()
-            .field(PassData.FieldKeys.title, .string, .required)
-            .field(PassData.FieldKeys.passID, .uuid, .required, .references(Pass.schema, .id, onDelete: .cascade))
-            .create()
-    }
-
-    func revert(on database: any Database) async throws {
-        try await database.schema(PassData.FieldKeys.schemaName).delete()
-    }
-}
-
-extension PassData {
-    enum FieldKeys {
-        static let schemaName = "pass_data"
-        static let title = FieldKey(stringLiteral: "title")
-        static let passID = FieldKey(stringLiteral: "pass_id")
-    }
-}
 
 extension PassJSON.FormatVersion: Decodable {}
 extension PassJSON.BarcodeFormat: Decodable {}
@@ -54,7 +8,7 @@ struct PassJSONData: PassJSON.Properties, Decodable {
     let description: String
     let formatVersion = PassJSON.FormatVersion.v1
     let organizationName = "vapor-community"
-    let passTypeIdentifier = "pass.com.vapor-community.PassKit"
+    let passTypeIdentifier = PassData.typeIdentifier
     let serialNumber: String
     let teamIdentifier = "K6512ZA2S5"
 
@@ -116,29 +70,3 @@ struct PassJSONData: PassJSON.Properties, Decodable {
         self.authenticationToken = pass.authenticationToken
     }
 }
-
-struct PassDataMiddleware: AsyncModelMiddleware {
-    private unowned let service: PassesService
-
-    init(service: PassesService) {
-        self.service = service
-    }
-
-    func create(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws {
-        let pass = Pass(
-            typeIdentifier: "pass.com.vapor-community.PassKit",
-            authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
-        )
-        try await pass.save(on: db)
-        model.$pass.id = try pass.requireID()
-        try await next.create(model, on: db)
-    }
-
-    func update(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws {
-        let pass = try await model.$pass.get(on: db)
-        pass.updatedAt = Date()
-        try await pass.save(on: db)
-        try await next.update(model, on: db)
-        try await service.sendPushNotifications(for: pass, on: db)
-    }
-}
diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/Utils/withApp.swift
similarity index 77%
rename from Tests/PassesTests/withApp.swift
rename to Tests/PassesTests/Utils/withApp.swift
index 4681421..b5ac620 100644
--- a/Tests/PassesTests/withApp.swift
+++ b/Tests/PassesTests/Utils/withApp.swift
@@ -8,21 +8,19 @@ import Zip
 
 func withApp(
     useEncryptedKey: Bool = false,
-    _ body: (Application, PassesService) async throws -> Void
+    _ body: (Application, PassesService<PassData>) async throws -> Void
 ) async throws {
     let app = try await Application.make(.testing)
     do {
         try #require(isLoggingConfigured)
 
         app.databases.use(.sqlite(.memory), as: .sqlite)
-        PassesService.register(migrations: app.migrations)
+        PassesService<PassData>.register(migrations: app.migrations)
         app.migrations.add(CreatePassData())
         try await app.autoMigrate()
 
-        let delegate = TestPassesDelegate()
-        let passesService = try PassesService(
+        let passesService = try PassesService<PassData>(
             app: app,
-            delegate: delegate,
             pushRoutesMiddleware: SecretMiddleware(secret: "foo"),
             logger: app.logger,
             pemWWDRCertificate: TestCertificate.pemWWDRCertificate,
@@ -30,7 +28,7 @@ func withApp(
             pemPrivateKey: useEncryptedKey ? TestCertificate.encryptedPemPrivateKey : TestCertificate.pemPrivateKey,
             pemPrivateKeyPassword: useEncryptedKey ? "password" : nil
         )
-        app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite)
+        app.databases.middleware.use(passesService, on: .sqlite)
 
         Zip.addCustomFileExtension("pkpass")
 
@@ -38,6 +36,7 @@ func withApp(
 
         try await app.autoRevert()
     } catch {
+        try? await app.autoRevert()
         try await app.asyncShutdown()
         throw error
     }