1use std::collections::BTreeMap;
4
5use super::registration::{CreditRegistration, is_waiting_for_enrolment};
6use super::state::{CreditRegistrationErrorCode, CreditRegistrationState, ResubmissionFacts};
7use super::teacher_view::search_pattern_of;
8use crate::credit_registration_enrolment_routes::CreditRegistrationEnrolmentRoute;
9use crate::library::credit_registration::enrolment_check_schedule::EnrolmentCheckSource;
10use crate::library::credit_registration::timeline::{
11 Engagement, StepMatch, TimelinePosition, TimelineStep,
12};
13use crate::library::credit_registration::{CreditRegistrationPendingReason, PendingPreconditions};
14use crate::prelude::*;
15use crate::verified_student_numbers::StudentNumberVerificationMethod;
16
17#[derive(Debug, Clone)]
23pub struct AdminCreditRegistration {
24 pub id: Uuid,
25 pub created_at: DateTime<Utc>,
26 pub user_id: Uuid,
27 pub first_name: Option<String>,
28 pub last_name: Option<String>,
29 pub email: Option<String>,
31 pub course_id: Uuid,
32 pub course_name: String,
33 pub course_module_id: Uuid,
34 pub course_module_name: Option<String>,
35 pub course_instance_id: Uuid,
36 pub course_module_completion_id: Uuid,
37 pub completion_date: DateTime<Utc>,
38 pub state: CreditRegistrationState,
39 pub state_entered_at: DateTime<Utc>,
40 pub state_changed_at: DateTime<Utc>,
41 pub phase_started_at: DateTime<Utc>,
42 pub error_code: Option<CreditRegistrationErrorCode>,
43 pub needs_admin_attention: bool,
44 pub next_attempt_at: DateTime<Utc>,
45 pub last_attempt_at: Option<DateTime<Utc>>,
46 pub submitted_at: Option<DateTime<Utc>>,
47 pub registered_at: Option<DateTime<Utc>>,
48 pub terminal_at: Option<DateTime<Utc>>,
49 pub student_number: Option<DbSecret>,
51 pub sisu_person_id: Option<DbSecret>,
52 pub uh_course_code: Option<String>,
53 pub selected_enrolment_id: Option<String>,
54 pub grade_scale_id: Option<String>,
55 pub grade_id: Option<String>,
56 pub credits: Option<f32>,
57 pub submitted_attainment_id: Option<String>,
58 pub sisu_attainment_id: Option<String>,
59 pub submit_retry_count: i32,
60 pub verify_attempt_count: i32,
61 pub attempt_number: i32,
62 pub superseded_by_id: Option<Uuid>,
63 pub verified_student_number: Option<DbSecret>,
65 pub verified_student_number_at: Option<DateTime<Utc>>,
66 pub verified_student_number_via: Option<StudentNumberVerificationMethod>,
67 pub resubmit_not_before: Option<DateTime<Utc>>,
68 pub partially_registered_at: Option<DateTime<Utc>>,
69 pub not_registered_reimport_count: i32,
70 pub no_usable_enrolment_since: Option<DateTime<Utc>>,
71 pub enrolment_checked_at: Option<DateTime<Utc>>,
72 pub enrolment_check_anchor_at: Option<DateTime<Utc>>,
73 pub enrolment_check_due_at: Option<DateTime<Utc>>,
74 pub enrolment_checks_stopped_at: Option<DateTime<Utc>>,
75 pub completion_eligible: bool,
76 pub has_verified_student_number: bool,
77 pub course_code_allowed: bool,
78 pub pressed_at: Option<DateTime<Utc>>,
80 pub enrolment_route: Option<CreditRegistrationEnrolmentRoute>,
81 pub last_visited_at: Option<DateTime<Utc>>,
82 pub total_count: i64,
85}
86
87impl AdminCreditRegistration {
88 pub fn resubmission_facts(&self) -> ResubmissionFacts {
90 ResubmissionFacts {
91 state: self.state,
92 is_superseded: self.superseded_by_id.is_some(),
93 error_code: self.error_code,
94 resubmit_not_before: self.resubmit_not_before,
95 submitted_at: self.submitted_at,
96 }
97 }
98
99 pub fn is_waiting_for_enrolment(&self) -> bool {
101 is_waiting_for_enrolment(
102 self.state,
103 self.enrolment_check_anchor_at,
104 self.no_usable_enrolment_since,
105 )
106 }
107
108 pub fn preconditions(&self) -> PendingPreconditions {
110 PendingPreconditions {
111 completion_eligible: self.completion_eligible,
112 has_verified_student_number: self.has_verified_student_number,
113 course_code_allowed: self.course_code_allowed,
114 }
115 }
116
117 pub fn position(&self) -> TimelinePosition {
119 TimelinePosition::of(
120 self.state,
121 self.preconditions(),
122 self.selected_enrolment_id.is_some(),
123 Engagement::of(self.pressed_at, self.last_visited_at),
124 )
125 }
126
127 pub fn phase_started_at(&self) -> DateTime<Utc> {
130 match self.position().step {
131 TimelineStep::HeldForCourseCode => self
132 .verified_student_number_at
133 .map_or(self.phase_started_at, |linked_at| {
134 linked_at.max(self.phase_started_at)
135 }),
136 _ => self.phase_started_at,
137 }
138 }
139
140 pub fn pending_reason(&self) -> Option<CreditRegistrationPendingReason> {
143 (self.state == CreditRegistrationState::Pending)
144 .then(|| self.preconditions().reason())
145 .flatten()
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151pub enum AdminCreditRegistrationSort {
152 #[default]
153 LastActivity,
154 Created,
155 TimeInState,
156 Attempts,
157}
158
159impl AdminCreditRegistrationSort {
160 fn as_str(self) -> &'static str {
162 match self {
163 Self::LastActivity => "last_activity",
164 Self::Created => "created",
165 Self::TimeInState => "time_in_state",
166 Self::Attempts => "attempts",
167 }
168 }
169}
170
171#[derive(Debug, Clone, Default)]
173pub struct AdminCreditRegistrationFilters<'a> {
174 pub states: Option<&'a [CreditRegistrationState]>,
175 pub error_codes: Option<&'a [CreditRegistrationErrorCode]>,
176 pub course_id: Option<Uuid>,
177 pub course_module_id: Option<Uuid>,
178 pub user_id: Option<Uuid>,
179 pub student_number: Option<&'a str>,
180 pub steps: &'a [TimelineStep],
184 pub engagements: &'a [Engagement],
187 pub hide_not_started: bool,
189 pub account_linking_since: Option<DateTime<Utc>>,
190 pub first_ids: Option<&'a [Uuid]>,
192 pub submitted_after: Option<DateTime<Utc>>,
193 pub submitted_before: Option<DateTime<Utc>>,
194 pub search: Option<&'a str>,
197 pub search_id: Option<Uuid>,
201 pub id: Option<Uuid>,
203 pub course_module_completion_id: Option<Uuid>,
205 pub credit_registration_ids: Option<&'a [Uuid]>,
207 pub include_superseded: bool,
209}
210
211async fn admin_facing_page(
214 conn: &mut PgConnection,
215 filters: &AdminCreditRegistrationFilters<'_>,
216 sort: AdminCreditRegistrationSort,
217 limit: i64,
218 offset: i64,
219) -> ModelResult<Vec<AdminCreditRegistration>> {
220 let search_pattern = filters.search.map(search_pattern_of);
221 let user_search = crate::user_details::UserListSearch::new(filters.search.unwrap_or_default());
222 let step_match = StepMatch::all();
223 let res = sqlx::query_as!(
224 AdminCreditRegistration,
225 r#"
226SELECT cr.id,
227 cr.created_at,
228 cr.user_id,
229 ud.first_name AS "first_name?",
230 ud.last_name AS "last_name?",
231 ud.email AS "email?",
232 cr.course_id,
233 c.name AS course_name,
234 cr.course_module_id,
235 cm.name AS course_module_name,
236 cr.course_instance_id,
237 cr.course_module_completion_id,
238 cmc.completion_date,
239 cr.state,
240 cr.state_entered_at,
241 cr.state_changed_at,
242 cr.phase_started_at,
243 cr.error_code AS "error_code?",
244 cr.needs_admin_attention,
245 cr.next_attempt_at,
246 cr.last_attempt_at,
247 cr.submitted_at,
248 cr.registered_at,
249 cr.terminal_at,
250 cr.student_number,
251 cr.sisu_person_id,
252 cr.uh_course_code,
253 cr.selected_enrolment_id,
254 cr.grade_scale_id,
255 cr.grade_id,
256 cr.credits,
257 cr.submitted_attainment_id,
258 cr.sisu_attainment_id,
259 cr.submit_retry_count,
260 cr.verify_attempt_count,
261 cr.attempt_number,
262 cr.superseded_by_id,
263 vsn.student_number AS "verified_student_number?",
264 vsn.verified_at AS "verified_student_number_at?",
265 vsn.verified_via AS "verified_student_number_via?",
266 cr.resubmit_not_before,
267 cr.partially_registered_at,
268 cr.not_registered_reimport_count,
269 cr.no_usable_enrolment_since,
270 cr.enrolment_checked_at,
271 cr.enrolment_check_anchor_at,
272 cr.enrolment_check_due_at,
273 cr.enrolment_checks_stopped_at,
274 p.completion_eligible AS "completion_eligible!",
275 p.has_verified_student_number AS "has_verified_student_number!",
276 p.course_code_allowed AS "course_code_allowed!",
277 route.enrolment_confirmed_at AS "pressed_at?",
278 route.route AS "enrolment_route?: CreditRegistrationEnrolmentRoute",
279 sig.last_visited_at AS "last_visited_at?",
280 COUNT(*) OVER () AS "total_count!"
281FROM credit_registrations cr
282 JOIN courses c ON c.id = cr.course_id
283 JOIN course_modules cm ON cm.id = cr.course_module_id
284 JOIN course_module_completions cmc ON cmc.id = cr.course_module_completion_id
285 JOIN credit_registration_preconditions p ON p.credit_registration_id = cr.id
286 LEFT JOIN user_details ud ON ud.user_id = cr.user_id
287 LEFT JOIN verified_student_numbers vsn ON vsn.user_id = cr.user_id
288 AND vsn.deleted_at IS NULL
289 LEFT JOIN credit_registration_enrolment_routes route ON route.course_module_completion_id = cr.course_module_completion_id
290 AND route.deleted_at IS NULL
291 LEFT JOIN credit_registration_enrolment_check_signals sig ON sig.course_module_completion_id = cr.course_module_completion_id
292 AND sig.deleted_at IS NULL
293 -- The step and engagement are computed exactly as in count_by_step_and_engagement, so a list
294 -- filtered to a step and engagement holds as many rows as that function counts.
295 LEFT JOIN LATERAL (
296 SELECT m.step
297 FROM UNNEST(
298 $19::credit_registration_state [],
299 $20::boolean [],
300 $21::boolean [],
301 $22::boolean [],
302 $23::boolean [],
303 $24::credit_registration_timeline_step []
304 ) AS m(
305 state,
306 completion_eligible,
307 has_verified_student_number,
308 course_code_allowed,
309 enrolment_resolved,
310 step
311 )
312 WHERE m.state = cr.state
313 AND m.completion_eligible = p.completion_eligible
314 AND m.has_verified_student_number = p.has_verified_student_number
315 AND m.course_code_allowed = p.course_code_allowed
316 AND m.enrolment_resolved = (cr.selected_enrolment_id IS NOT NULL)
317 ) ts ON TRUE
318 CROSS JOIN LATERAL (
319 SELECT CASE
320 WHEN ts.step = ANY($25::credit_registration_timeline_step []) THEN CASE
321 WHEN route.enrolment_confirmed_at IS NOT NULL THEN 'pressed'::credit_registration_engagement
322 WHEN sig.last_visited_at IS NOT NULL THEN 'visited'::credit_registration_engagement
323 ELSE 'not_started'::credit_registration_engagement
324 END
325 END AS engagement,
326 ts.step = 'waiting_for_student_number'
327 AND cmc.completion_date < $29::timestamptz AS is_before_linking
328 ) eng
329WHERE cr.deleted_at IS NULL
330 AND ($1::bool OR cr.superseded_by_id IS NULL)
331 AND (
332 $2::credit_registration_state [] IS NULL
333 OR cr.state = ANY($2)
334 )
335 AND (
336 $3::credit_registration_error_code [] IS NULL
337 OR cr.error_code = ANY($3)
338 )
339 AND ($4::uuid IS NULL OR cr.course_id = $4)
340 AND ($5::uuid IS NULL OR cr.course_module_id = $5)
341 AND ($6::uuid IS NULL OR cr.user_id = $6)
342 AND (
343 $7::text IS NULL
344 OR cr.student_number = $7
345 OR vsn.student_number = $7
346 )
347 AND (
348 CARDINALITY($8::credit_registration_timeline_step []) = 0
349 OR (
350 ts.step = ANY($8)
351 AND eng.is_before_linking IS NOT TRUE
352 )
353 )
354 AND (
355 CARDINALITY($26::credit_registration_engagement []) = 0
356 OR eng.engagement = ANY($26)
357 )
358 AND (
359 NOT $27::bool
360 OR eng.engagement IS DISTINCT FROM 'not_started'
361 )
362 AND ($9::timestamptz IS NULL OR cr.submitted_at >= $9)
363 AND ($10::timestamptz IS NULL OR cr.submitted_at <= $10)
364 AND (
365 $11::text IS NULL
366 OR (ud.name_search_helper || ' ' || ud.email_search_helper) LIKE ALL ($30::text [])
367 OR word_similarity($31::text, ud.name_search_helper || ' ' || ud.email_search_helper) >= 0.5
368 OR LOWER(cr.student_number) LIKE '%' || $11 || '%' ESCAPE '\'
369 OR LOWER(vsn.student_number) LIKE '%' || $11 || '%' ESCAPE '\'
370 OR LOWER(cr.submitted_attainment_id) LIKE '%' || $11 || '%' ESCAPE '\'
371 OR LOWER(cr.sisu_attainment_id) LIKE '%' || $11 || '%' ESCAPE '\'
372 OR LOWER(cr.error_message) LIKE '%' || $11 || '%' ESCAPE '\'
373 )
374 AND (
375 $12::uuid IS NULL
376 OR cr.id = $12
377 OR cr.user_id = $12
378 OR cr.course_module_completion_id = $12
379 )
380 AND (
381 $13::uuid [] IS NULL
382 OR cr.id = ANY($13)
383 )
384 AND ($17::uuid IS NULL OR cr.id = $17)
385 AND (
386 $18::uuid IS NULL
387 OR cr.course_module_completion_id = $18
388 )
389ORDER BY COALESCE(cr.id = ANY($28::uuid []), FALSE) DESC,
390 CASE
391 WHEN $14::text = 'attempts' THEN cr.submit_retry_count + cr.verify_attempt_count
392 END DESC NULLS LAST,
393 CASE $14::text
394 WHEN 'created' THEN cr.created_at
395 WHEN 'time_in_state' THEN cr.state_changed_at
396 ELSE COALESCE(cr.last_attempt_at, cr.state_entered_at)
397 END DESC,
398 cr.id
399LIMIT $15 OFFSET $16
400 "#,
401 filters.include_superseded,
402 filters.states as Option<&[CreditRegistrationState]>,
403 filters.error_codes as Option<&[CreditRegistrationErrorCode]>,
404 filters.course_id,
405 filters.course_module_id,
406 filters.user_id,
407 filters.student_number,
408 filters.steps as &[TimelineStep],
409 filters.submitted_after,
410 filters.submitted_before,
411 search_pattern.as_deref(),
412 filters.search_id,
413 filters.credit_registration_ids as Option<&[Uuid]>,
414 sort.as_str(),
415 limit,
416 offset,
417 filters.id,
418 filters.course_module_completion_id,
419 &step_match.states as &[CreditRegistrationState],
420 &step_match.completion_eligible,
421 &step_match.has_verified_student_number,
422 &step_match.course_code_allowed,
423 &step_match.enrolment_resolved,
424 &step_match.steps as &[TimelineStep],
425 &TimelineStep::ENGAGEMENT_STEPS as &[TimelineStep],
426 filters.engagements as &[Engagement],
427 filters.hide_not_started,
428 filters.first_ids as Option<&[Uuid]>,
429 filters.account_linking_since,
430 &user_search.word_patterns,
431 user_search.misspelling_term.as_deref(),
432 )
433 .fetch_all(conn)
434 .await?;
435 Ok(res)
436}
437
438pub async fn get_admin_facing(
440 conn: &mut PgConnection,
441 filters: &AdminCreditRegistrationFilters<'_>,
442 sort: AdminCreditRegistrationSort,
443 limit: i64,
444 offset: i64,
445) -> ModelResult<Vec<AdminCreditRegistration>> {
446 admin_facing_page(conn, filters, sort, limit, offset).await
447}
448
449#[derive(Debug, Clone, PartialEq)]
451pub struct StepCount {
452 pub course_module_id: Uuid,
453 pub step: TimelineStep,
454 pub engagement: Option<Engagement>,
455 pub count: i64,
456}
457
458pub async fn count_by_step_and_engagement(
465 conn: &mut PgConnection,
466 account_linking_since: Option<DateTime<Utc>>,
467) -> ModelResult<Vec<StepCount>> {
468 let step_match = StepMatch::all();
469 let res = sqlx::query_as!(
470 StepCount,
471 r#"
472SELECT cr.course_module_id,
473 ts.step AS "step!: TimelineStep",
474 eng.engagement AS "engagement?: Engagement",
475 COUNT(*) AS "count!"
476FROM credit_registrations cr
477 JOIN course_module_completions cmc ON cmc.id = cr.course_module_completion_id
478 JOIN credit_registration_preconditions p ON p.credit_registration_id = cr.id
479 LEFT JOIN credit_registration_enrolment_routes route ON route.course_module_completion_id = cr.course_module_completion_id
480 AND route.deleted_at IS NULL
481 LEFT JOIN credit_registration_enrolment_check_signals sig ON sig.course_module_completion_id = cr.course_module_completion_id
482 AND sig.deleted_at IS NULL
483 -- Computed exactly as in admin_facing_page; see there.
484 JOIN LATERAL (
485 SELECT m.step
486 FROM UNNEST(
487 $1::credit_registration_state [],
488 $2::boolean [],
489 $3::boolean [],
490 $4::boolean [],
491 $5::boolean [],
492 $6::credit_registration_timeline_step []
493 ) AS m(
494 state,
495 completion_eligible,
496 has_verified_student_number,
497 course_code_allowed,
498 enrolment_resolved,
499 step
500 )
501 WHERE m.state = cr.state
502 AND m.completion_eligible = p.completion_eligible
503 AND m.has_verified_student_number = p.has_verified_student_number
504 AND m.course_code_allowed = p.course_code_allowed
505 AND m.enrolment_resolved = (cr.selected_enrolment_id IS NOT NULL)
506 ) ts ON TRUE
507 CROSS JOIN LATERAL (
508 SELECT CASE
509 WHEN ts.step = ANY($7::credit_registration_timeline_step []) THEN CASE
510 WHEN route.enrolment_confirmed_at IS NOT NULL THEN 'pressed'::credit_registration_engagement
511 WHEN sig.last_visited_at IS NOT NULL THEN 'visited'::credit_registration_engagement
512 ELSE 'not_started'::credit_registration_engagement
513 END
514 END AS engagement,
515 ts.step = 'waiting_for_student_number'
516 AND cmc.completion_date < $8::timestamptz AS is_before_linking
517 ) eng
518WHERE cr.deleted_at IS NULL
519 AND cr.superseded_by_id IS NULL
520 AND eng.is_before_linking IS NOT TRUE
521GROUP BY cr.course_module_id,
522 ts.step,
523 eng.engagement
524 "#,
525 &step_match.states as &[CreditRegistrationState],
526 &step_match.completion_eligible,
527 &step_match.has_verified_student_number,
528 &step_match.course_code_allowed,
529 &step_match.enrolment_resolved,
530 &step_match.steps as &[TimelineStep],
531 &TimelineStep::ENGAGEMENT_STEPS as &[TimelineStep],
532 account_linking_since,
533 )
534 .fetch_all(conn)
535 .await?;
536 Ok(res)
537}
538
539pub fn sum_step_counts_over_modules<'a>(
541 rows: impl IntoIterator<Item = &'a StepCount>,
542) -> BTreeMap<(TimelineStep, Option<Engagement>), i64> {
543 let mut totals = BTreeMap::new();
544 for row in rows {
545 *totals.entry((row.step, row.engagement)).or_insert(0) += row.count;
546 }
547 totals
548}
549
550pub async fn get_live_by_states(
554 conn: &mut PgConnection,
555 states: &[CreditRegistrationState],
556 limit_per_state: i64,
557) -> ModelResult<Vec<CreditRegistration>> {
558 let res = sqlx::query_as!(
559 CreditRegistration,
560 r#"
561SELECT cr.*
562FROM credit_registrations cr
563 JOIN (
564 SELECT id,
565 ROW_NUMBER() OVER (
566 PARTITION BY state
567 ORDER BY state_entered_at DESC
568 ) AS rn
569 FROM credit_registrations
570 WHERE state = ANY($1)
571 AND superseded_by_id IS NULL
572 AND deleted_at IS NULL
573 ) ranked ON ranked.id = cr.id
574WHERE ranked.rn <= $2
575ORDER BY cr.state_entered_at DESC
576 "#,
577 states as &[CreditRegistrationState],
578 limit_per_state,
579 )
580 .fetch_all(conn)
581 .await?;
582 Ok(res)
583}
584
585#[derive(Debug, Clone, PartialEq)]
587pub struct AdminRegistrationJourney {
588 pub course_started_at: Option<DateTime<Utc>>,
590 pub first_visited_at: Option<DateTime<Utc>>,
593 pub last_visited_at: Option<DateTime<Utc>>,
594 pub last_check_requested_at: Option<DateTime<Utc>>,
595 pub check_request_source: Option<EnrolmentCheckSource>,
596 pub enrolment_route: Option<CreditRegistrationEnrolmentRoute>,
597 pub pressed_at: Option<DateTime<Utc>>,
598 pub checked_enrolled_at: Option<DateTime<Utc>>,
600 pub selected_enrolment_date_time: Option<String>,
602}
603
604pub async fn get_registration_journey(
606 conn: &mut PgConnection,
607 credit_registration_id: Uuid,
608) -> ModelResult<Option<AdminRegistrationJourney>> {
609 let res = sqlx::query_as!(
610 AdminRegistrationJourney,
611 r#"
612SELECT ucs.created_at AS "course_started_at?",
613 CASE
614 WHEN sig.last_visited_at IS NOT NULL THEN LEAST(sig.created_at, sig.last_visited_at)
615 END AS "first_visited_at?",
616 sig.last_visited_at AS "last_visited_at?",
617 sig.last_check_requested_at AS "last_check_requested_at?",
618 sig.check_request_source AS "check_request_source?: EnrolmentCheckSource",
619 route.route AS "enrolment_route?: CreditRegistrationEnrolmentRoute",
620 route.enrolment_confirmed_at AS "pressed_at?",
621 (
622 SELECT o.enrolled_at
623 FROM credit_registration_enrolment_check_outcomes o
624 JOIN credit_registrations attempt ON attempt.id = o.credit_registration_id
625 WHERE attempt.course_module_completion_id = cr.course_module_completion_id
626 AND o.is_enrolment_found
627 AND o.enrolled_at IS NOT NULL
628 ORDER BY o.checked_at DESC
629 LIMIT 1
630 ) AS "checked_enrolled_at?",
631 (
632 SELECT found.value #>> '{}'
633 FROM credit_registration_events e
634 CROSS JOIN LATERAL jsonb_path_query_first(
635 e.details,
636 'lax $.** ? (@.id == $id).enrolmentDateTime',
637 jsonb_build_object('id', cr.selected_enrolment_id)
638 ) AS found(value)
639 WHERE e.credit_registration_id = cr.id
640 AND e.deleted_at IS NULL
641 AND cr.selected_enrolment_id IS NOT NULL
642 AND found.value IS NOT NULL
643 ORDER BY e.created_at DESC
644 LIMIT 1
645 ) AS "selected_enrolment_date_time?"
646FROM credit_registrations cr
647 JOIN courses c ON c.id = cr.course_id
648 LEFT JOIN user_course_settings ucs ON ucs.user_id = cr.user_id
649 AND ucs.course_language_group_id = c.course_language_group_id
650 AND ucs.deleted_at IS NULL
651 LEFT JOIN credit_registration_enrolment_check_signals sig ON sig.course_module_completion_id = cr.course_module_completion_id
652 AND sig.deleted_at IS NULL
653 LEFT JOIN credit_registration_enrolment_routes route ON route.course_module_completion_id = cr.course_module_completion_id
654 AND route.deleted_at IS NULL
655WHERE cr.id = $1
656 AND cr.deleted_at IS NULL
657 "#,
658 credit_registration_id,
659 )
660 .fetch_optional(conn)
661 .await?;
662 Ok(res)
663}