@@ -5,7 +5,7 @@ import { generateId } from "../core/utils";
55import type { PayKitDatabase } from "../database" ;
66import { feature , product , productFeature } from "../database/schema" ;
77import type { StoredFeature , StoredProduct , StoredProductFeature } from "../types/models" ;
8- import type { NormalizedFeature , NormalizedPlanFeature } from "../types/schema" ;
8+ import type { NormalizedFeature , NormalizedPlan , NormalizedPlanFeature } from "../types/schema" ;
99
1010export interface StoredProductSnapshot {
1111 features : readonly StoredProductFeature [ ] ;
@@ -109,6 +109,62 @@ export async function getProductByHash(
109109 return result ?? null ;
110110}
111111
112+ function serializeFeatureConfig ( config : Record < string , unknown > | null ) : string {
113+ return JSON . stringify ( config ?? null ) ;
114+ }
115+
116+ function productFeaturesMatch (
117+ existing : readonly StoredProductFeature [ ] ,
118+ next : readonly NormalizedPlanFeature [ ] ,
119+ ) : boolean {
120+ if ( existing . length !== next . length ) {
121+ return false ;
122+ }
123+
124+ return existing . every ( ( storedFeature , index ) => {
125+ const nextFeature = next [ index ] ;
126+ return (
127+ nextFeature != null &&
128+ storedFeature . featureId === nextFeature . id &&
129+ storedFeature . limit === nextFeature . limit &&
130+ storedFeature . resetInterval === nextFeature . resetInterval &&
131+ serializeFeatureConfig ( storedFeature . config ) === serializeFeatureConfig ( nextFeature . config )
132+ ) ;
133+ } ) ;
134+ }
135+
136+ function productSnapshotMatchesPlan (
137+ snapshot : StoredProductSnapshot ,
138+ plan : NormalizedPlan ,
139+ ) : boolean {
140+ return (
141+ snapshot . product . group === plan . group &&
142+ snapshot . product . isDefault === plan . isDefault &&
143+ ( snapshot . product . priceAmount ?? null ) === plan . priceAmount &&
144+ ( snapshot . product . priceCurrency ?? null ) === plan . priceCurrency &&
145+ ( snapshot . product . priceInterval ?? null ) === plan . priceInterval &&
146+ productFeaturesMatch ( snapshot . features , plan . includes )
147+ ) ;
148+ }
149+
150+ /** Finds the stored product for a normalized plan, including old hash-compatible rows. */
151+ export async function getProductByPlan (
152+ database : PayKitDatabase ,
153+ plan : NormalizedPlan ,
154+ ) : Promise < StoredProduct | null > {
155+ const exact = await getProductByHash ( database , plan . id , plan . hash ) ;
156+ if ( exact ) {
157+ return exact ;
158+ }
159+
160+ const latest = await getLatestProductSnapshot ( database , plan . id ) ;
161+ if ( ! latest || ! productSnapshotMatchesPlan ( latest , plan ) ) {
162+ return null ;
163+ }
164+
165+ return latest . product ;
166+ }
167+
112168export async function getProductByInternalId (
113169 database : PayKitDatabase ,
114170 internalId : string ,
0 commit comments