@@ -22,6 +22,7 @@ let getPledges: CampaignStoreModule['getPledges'];
2222let getDb : DbModule [ 'getDb' ] ;
2323let getCampaignHistory : EventHistoryModule [ 'getCampaignHistory' ] ;
2424let addPledge : CampaignStoreModule [ 'addPledge' ] ;
25+ let findNearDuplicate : CampaignStoreModule [ 'findNearDuplicate' ] ;
2526
2627const CREATOR = `G${ 'A' . repeat ( 55 ) } ` ;
2728const CONTRIBUTOR = `G${ 'B' . repeat ( 55 ) } ` ;
@@ -41,6 +42,7 @@ beforeAll(async () => {
4142 getCampaign,
4243 getPledges,
4344 addPledge,
45+ findNearDuplicate,
4446 } = await import ( './campaignStore' ) ) ;
4547 ( { getDb } = await import ( './db' ) ) ;
4648 ( { getCampaignHistory } = await import ( './eventHistory' ) ) ;
@@ -164,6 +166,73 @@ describe('on-chain pledge reconciliation', () => {
164166 } ) ;
165167} ) ;
166168
169+ describe ( 'near-duplicate detection' , ( ) => {
170+ const FUTURE_DEADLINE = Math . floor ( Date . now ( ) / 1000 ) + 86400 ;
171+
172+ it ( 'returns duplicate when same creator creates near-identical title' , ( ) => {
173+ createCampaign ( {
174+ creator : CREATOR ,
175+ title : 'Help me build a rocket' ,
176+ description : 'desc' ,
177+ assetCode : 'USDC' ,
178+ targetAmount : 100 ,
179+ deadline : FUTURE_DEADLINE ,
180+ } ) ;
181+
182+ const result = findNearDuplicate ( 'Help me build a rocket!!' , CREATOR ) ;
183+ expect ( result ) . toBeDefined ( ) ;
184+ expect ( result ! . distance ) . toBeLessThan ( 5 ) ;
185+ } ) ;
186+
187+ it ( 'allows same title from different creator' , ( ) => {
188+ createCampaign ( {
189+ creator : CREATOR ,
190+ title : 'Fund my project' ,
191+ description : 'desc' ,
192+ assetCode : 'USDC' ,
193+ targetAmount : 100 ,
194+ deadline : FUTURE_DEADLINE ,
195+ } ) ;
196+
197+ const otherCreator = `G${ 'D' . repeat ( 55 ) } ` ;
198+ const result = findNearDuplicate ( 'Fund my project' , otherCreator ) ;
199+ expect ( result ) . toBeUndefined ( ) ;
200+ } ) ;
201+
202+ it ( 'returns undefined when title is sufficiently different' , ( ) => {
203+ createCampaign ( {
204+ creator : CREATOR ,
205+ title : 'Completely different topic' ,
206+ description : 'desc' ,
207+ assetCode : 'USDC' ,
208+ targetAmount : 100 ,
209+ deadline : FUTURE_DEADLINE ,
210+ } ) ;
211+
212+ const result = findNearDuplicate ( 'Totally unrelated campaign' , CREATOR ) ;
213+ expect ( result ) . toBeUndefined ( ) ;
214+ } ) ;
215+
216+ it ( 'ignores campaigns older than 24 hours' , ( ) => {
217+ const db = getDb ( ) ;
218+ const oldTimestamp = Math . floor ( Date . now ( ) / 1000 ) - 60 * 60 * 25 ;
219+
220+ const campaign = createCampaign ( {
221+ creator : CREATOR ,
222+ title : 'Old campaign title' ,
223+ description : 'desc' ,
224+ assetCode : 'USDC' ,
225+ targetAmount : 100 ,
226+ deadline : FUTURE_DEADLINE ,
227+ } ) ;
228+
229+ db . prepare ( `UPDATE campaigns SET created_at = ? WHERE id = ?` ) . run ( oldTimestamp , campaign . id ) ;
230+
231+ const result = findNearDuplicate ( 'Old campaign title' , CREATOR ) ;
232+ expect ( result ) . toBeUndefined ( ) ;
233+ } ) ;
234+ } ) ;
235+
167236describe ( 'campaign pledge pagination' , ( ) => {
168237 it ( 'returns pledges in reverse chronological order with pagination metadata inputs' , ( ) => {
169238 const futureDeadline = Math . floor ( Date . now ( ) / 1000 ) + 86400 ;
0 commit comments