@@ -58,11 +58,16 @@ impl SqliteManager {
58
58
}
59
59
60
60
pub fn remove_tool_offering ( & self , tool_key : & str ) -> Result < ( ) , SqliteManagerError > {
61
- let conn = self . get_connection ( ) ?;
62
- let mut stmt = conn. prepare ( "DELETE FROM tool_micropayments_requirements WHERE tool_key = ?1" ) ?;
63
-
64
- stmt. execute ( params ! [ tool_key] ) ?;
65
-
61
+ let mut conn = self . get_connection ( ) ?;
62
+ let mut transaction = conn. transaction ( ) ?;
63
+
64
+ // First, nullify references in invoices to prevent constraint violations
65
+ transaction. execute ( "UPDATE invoices SET shinkai_offering_key = NULL WHERE shinkai_offering_key = ?1" , params ! [ tool_key] ) ?;
66
+
67
+ // Then delete the tool offering
68
+ transaction. execute ( "DELETE FROM tool_micropayments_requirements WHERE tool_key = ?1" , params ! [ tool_key] ) ?;
69
+
70
+ transaction. commit ( ) ?;
66
71
Ok ( ( ) )
67
72
}
68
73
@@ -320,4 +325,100 @@ mod tests {
320
325
. iter( )
321
326
. any( |offering| offering. tool_key == tool_offering2. tool_key) ) ;
322
327
}
328
+
329
+ #[ tokio:: test]
330
+ async fn test_remove_tool_offering_with_invoices ( ) {
331
+ use chrono:: Utc ;
332
+ use shinkai_message_primitives:: schemas:: invoices:: { Invoice , InvoiceStatusEnum } ;
333
+ use shinkai_message_primitives:: schemas:: shinkai_name:: ShinkaiName ;
334
+ use shinkai_message_primitives:: schemas:: shinkai_tool_offering:: UsageTypeInquiry ;
335
+ use shinkai_message_primitives:: schemas:: wallet_mixed:: PublicAddress ;
336
+ use rusqlite:: params;
337
+
338
+ let manager = setup_test_db ( ) ;
339
+ let tool_key = "test_tool_key_with_invoice" ;
340
+
341
+ // First, create a tool offering
342
+ let tool_offering = ShinkaiToolOffering {
343
+ tool_key : tool_key. to_string ( ) ,
344
+ usage_type : UsageType :: PerUse ( ToolPrice :: Payment ( vec ! [ PaymentRequirements {
345
+ scheme: "exact" . to_string( ) ,
346
+ description: "Payment for service" . to_string( ) ,
347
+ network: Network :: BaseSepolia ,
348
+ max_amount_required: "1000" . to_string( ) ,
349
+ resource: "https://shinkai.com" . to_string( ) ,
350
+ mime_type: "application/json" . to_string( ) ,
351
+ pay_to: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" . to_string( ) ,
352
+ max_timeout_seconds: 300 ,
353
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" . to_string( ) ,
354
+ output_schema: Some ( serde_json:: json!( { } ) ) ,
355
+ extra: Some ( serde_json:: json!( {
356
+ "decimals" : 6 ,
357
+ "asset_id" : "USDC"
358
+ } ) ) ,
359
+ } ] ) ) ,
360
+ meta_description : Some ( "meta_description" . to_string ( ) ) ,
361
+ } ;
362
+
363
+ // Insert the tool offering
364
+ let result = manager. set_tool_offering ( tool_offering. clone ( ) ) ;
365
+ assert ! ( result. is_ok( ) ) ;
366
+
367
+ // Create an invoice that references this tool offering
368
+ let invoice = Invoice {
369
+ invoice_id : "test_invoice_id" . to_string ( ) ,
370
+ parent_message_id : Some ( "test_parent_id" . to_string ( ) ) ,
371
+ provider_name : ShinkaiName :: new ( "@@provider.shinkai" . to_string ( ) ) . unwrap ( ) ,
372
+ requester_name : ShinkaiName :: new ( "@@requester.shinkai" . to_string ( ) ) . unwrap ( ) ,
373
+ shinkai_offering : tool_offering. clone ( ) ,
374
+ expiration_time : Utc :: now ( ) + chrono:: Duration :: hours ( 12 ) ,
375
+ status : InvoiceStatusEnum :: Pending ,
376
+ payment : None ,
377
+ address : PublicAddress {
378
+ network_id : Network :: BaseSepolia ,
379
+ address_id : "0x1234567890123456789012345678901234567890" . to_string ( ) ,
380
+ } ,
381
+ usage_type_inquiry : UsageTypeInquiry :: PerUse ,
382
+ request_date_time : Utc :: now ( ) ,
383
+ invoice_date_time : Utc :: now ( ) ,
384
+ tool_data : None ,
385
+ result_str : None ,
386
+ response_date_time : None ,
387
+ } ;
388
+
389
+ // Insert the invoice (this creates the foreign key reference)
390
+ let invoice_result = manager. set_invoice ( & invoice) ;
391
+ assert ! ( invoice_result. is_ok( ) ) ;
392
+
393
+ // Now try to remove the tool offering - this should succeed with the new implementation
394
+ let remove_result = manager. remove_tool_offering ( tool_key) ;
395
+ println ! ( "Remove tool offering result: {:?}" , remove_result) ;
396
+
397
+ // The removal should now succeed because we handle foreign key constraints properly
398
+ assert ! ( remove_result. is_ok( ) , "Tool offering removal should succeed" ) ;
399
+
400
+ // Verify that the tool offering was actually removed
401
+ let get_result = manager. get_tool_offering ( tool_key) ;
402
+ assert ! ( get_result. is_err( ) , "Tool offering should no longer exist" ) ;
403
+
404
+ // Verify that the invoice still exists but with shinkai_offering_key set to NULL
405
+ // Since get_invoice might not handle NULL foreign keys properly, let's check at the database level
406
+ let conn = manager. get_connection ( ) . unwrap ( ) ;
407
+ let mut stmt = conn. prepare ( "SELECT invoice_id, shinkai_offering_key FROM invoices WHERE invoice_id = ?1" ) . unwrap ( ) ;
408
+ let result: Result < ( String , Option < String > ) , _ > = stmt. query_row ( params ! [ "test_invoice_id" ] , |row| {
409
+ Ok ( ( row. get ( 0 ) ?, row. get ( 1 ) ?) )
410
+ } ) ;
411
+
412
+ match result {
413
+ Ok ( ( invoice_id, offering_key) ) => {
414
+ println ! ( "Invoice still exists: {}" , invoice_id) ;
415
+ assert_eq ! ( invoice_id, "test_invoice_id" ) ;
416
+ assert ! ( offering_key. is_none( ) , "shinkai_offering_key should be NULL after tool offering removal" ) ;
417
+ println ! ( "Verified: shinkai_offering_key is NULL as expected" ) ;
418
+ } ,
419
+ Err ( err) => {
420
+ panic ! ( "Invoice should still exist after tool offering removal: {:?}" , err) ;
421
+ }
422
+ }
423
+ }
323
424
}
0 commit comments