Skip to main content

pharos_runtime_database/
payment.rs

1//! Bounded payment settings reads through the configured app database.
2
3use std::collections::BTreeMap;
4
5use serde_json::{Map, Value};
6
7use crate::DbBackendConfig;
8use crate::operations::{
9    AppSchema, DatabaseError, DatabaseOperations, Mutation, Predicate, Query, SortKey, Transaction,
10    WriteValue,
11};
12
13fn declared_text<'a>(declaration: &'a Value, key: &str) -> Result<&'a str, DatabaseError> {
14    declaration
15        .get(key)
16        .and_then(Value::as_str)
17        .filter(|value| !value.is_empty() && value.len() <= 128)
18        .ok_or(DatabaseError::InvalidContract)
19}
20
21fn declared_fields(
22    schema: &AppSchema,
23    entity: &str,
24    names: &[&str],
25) -> Result<Vec<String>, DatabaseError> {
26    let declared = schema
27        .entities
28        .iter()
29        .find(|candidate| candidate.name == entity)
30        .ok_or(DatabaseError::InvalidContract)?;
31    let mut fields = Vec::new();
32    for name in names {
33        if !declared.fields.iter().any(|field| field.name == *name) {
34            return Err(DatabaseError::InvalidContract);
35        }
36        if !fields.iter().any(|field| field == name) {
37            fields.push((*name).to_owned());
38        }
39    }
40    Ok(fields)
41}
42
43/// Selects the full declared row only for internal lifecycle processing.
44fn all_fields(schema: &AppSchema, entity: &str) -> Result<Vec<String>, DatabaseError> {
45    Ok(schema
46        .entities
47        .iter()
48        .find(|definition| definition.name == entity)
49        .ok_or(DatabaseError::InvalidContract)?
50        .fields
51        .iter()
52        .map(|field| field.name.clone())
53        .collect())
54}
55
56fn one_row(
57    adapter: &dyn DatabaseOperations,
58    config: &DbBackendConfig,
59    schema: &AppSchema,
60    entity: &str,
61    fields: Vec<String>,
62    filter: Option<Predicate>,
63) -> Result<Option<Map<String, Value>>, DatabaseError> {
64    let mut rows = adapter.query(
65        config,
66        schema,
67        &Query {
68            entity: entity.to_owned(),
69            fields,
70            filter,
71            order: Vec::new(),
72            limit: 2,
73            offset: 0,
74        },
75    )?;
76    if rows.len() > 1 {
77        return Err(DatabaseError::Conflict);
78    }
79    Ok(rows.pop())
80}
81
82/// Reads only the owner and payment records needed to render provider settings.
83/// The caller supplies an app-owned owner declaration; no backend or app id is
84/// selected here. Secret ciphertext is replaced with a presence marker before
85/// the returned view leaves this module.
86pub fn payment_render_state(
87    adapter: &dyn DatabaseOperations,
88    config: &DbBackendConfig,
89    schema: &AppSchema,
90    declaration: &Value,
91    owner_route_value: Option<&str>,
92    provider_id: &str,
93) -> Result<Value, DatabaseError> {
94    payment_state(
95        adapter,
96        config,
97        schema,
98        declaration,
99        owner_route_value,
100        provider_id,
101        None,
102    )
103}
104
105/// Loads a bounded transaction view for one owner and authenticated actor.
106pub fn payment_lifecycle_state(
107    adapter: &dyn DatabaseOperations,
108    config: &DbBackendConfig,
109    schema: &AppSchema,
110    declaration: &Value,
111    owner_route_value: &str,
112    provider_id: &str,
113    actor_id: i64,
114) -> Result<Value, DatabaseError> {
115    if actor_id <= 0 {
116        return Err(DatabaseError::InvalidInput);
117    }
118    payment_state(
119        adapter,
120        config,
121        schema,
122        declaration,
123        Some(owner_route_value),
124        provider_id,
125        Some(actor_id),
126    )
127}
128
129fn payment_state(
130    adapter: &dyn DatabaseOperations,
131    config: &DbBackendConfig,
132    schema: &AppSchema,
133    declaration: &Value,
134    owner_route_value: Option<&str>,
135    provider_id: &str,
136    actor_id: Option<i64>,
137) -> Result<Value, DatabaseError> {
138    if provider_id.is_empty() || provider_id.len() > 64 {
139        return Err(DatabaseError::InvalidInput);
140    }
141    let owner_entity = declared_text(declaration, "owner_entity")?;
142    let owner_section = declared_text(declaration, "owner_state_section")?;
143    let owner_route_field = declared_text(declaration, "owner_route_field")?;
144    let owner_id_field = declared_text(declaration, "owner_id_field")?;
145    let owner_type = declared_text(declaration, "owner_resource_type")?;
146    let tenant_entity = declared_text(declaration, "tenant_entity")?;
147    let tenant_section = declared_text(declaration, "tenant_state_section")?;
148    let tenant_owner_id_field = declared_text(declaration, "tenant_owner_id_field")?;
149    let tenant_id_field = declared_text(declaration, "tenant_id_field")?;
150    let mut owner_names = vec![owner_route_field, owner_id_field];
151    if let Some(field) = declaration
152        .get("owner_user_id_field")
153        .and_then(Value::as_str)
154    {
155        owner_names.push(field);
156    }
157    if tenant_entity == owner_entity {
158        owner_names.push(tenant_owner_id_field);
159        owner_names.push(tenant_id_field);
160        if let Some(field) = declaration
161            .get("tenant_owner_user_id_field")
162            .and_then(Value::as_str)
163        {
164            owner_names.push(field);
165        }
166    }
167    let owner_fields = declared_fields(schema, owner_entity, &owner_names)?;
168    let owner_filter = owner_route_value
169        .map(|value| {
170            if value.is_empty() || value.len() > 128 {
171                return Err(DatabaseError::InvalidInput);
172            }
173            Ok(Predicate::Equal(
174                owner_route_field.to_owned(),
175                Value::String(value.to_owned()),
176            ))
177        })
178        .transpose()?;
179    let owner_result = one_row(
180        adapter,
181        config,
182        schema,
183        owner_entity,
184        owner_fields,
185        owner_filter,
186    );
187    if owner_route_value.is_none() && matches!(owner_result, Err(DatabaseError::Conflict)) {
188        return Ok(Value::Object(Map::new()));
189    }
190    let Some(owner) = owner_result? else {
191        return Ok(Value::Object(Map::new()));
192    };
193    let owner_id = owner
194        .get(owner_id_field)
195        .and_then(Value::as_i64)
196        .filter(|id| *id > 0)
197        .ok_or(DatabaseError::StorageFailure)?;
198    let tenant = if tenant_entity == owner_entity && tenant_section == owner_section {
199        owner.clone()
200    } else {
201        let mut tenant_names = vec![tenant_id_field, tenant_owner_id_field];
202        if let Some(field) = declaration
203            .get("tenant_owner_user_id_field")
204            .and_then(Value::as_str)
205        {
206            tenant_names.push(field);
207        }
208        let tenant_fields = declared_fields(schema, tenant_entity, &tenant_names)?;
209        let Some(row) = one_row(
210            adapter,
211            config,
212            schema,
213            tenant_entity,
214            tenant_fields,
215            Some(Predicate::Equal(
216                tenant_owner_id_field.to_owned(),
217                Value::from(owner_id),
218            )),
219        )?
220        else {
221            return Ok(Value::Object(Map::new()));
222        };
223        row
224    };
225    let tenant_id = tenant
226        .get(tenant_id_field)
227        .and_then(Value::as_i64)
228        .filter(|id| *id > 0)
229        .ok_or(DatabaseError::StorageFailure)?;
230    let scope = Predicate::All(vec![
231        Predicate::Equal("app_id".to_owned(), Value::String(schema.app_id.clone())),
232        Predicate::Equal("tenant_id".to_owned(), Value::from(tenant_id)),
233        Predicate::Equal(
234            "owner_resource_type".to_owned(),
235            Value::String(owner_type.to_owned()),
236        ),
237        Predicate::Equal("owner_resource_id".to_owned(), Value::from(owner_id)),
238        Predicate::Equal(
239            "provider_id".to_owned(),
240            Value::String(provider_id.to_owned()),
241        ),
242    ]);
243    let account_fields = if actor_id.is_some() {
244        all_fields(schema, "payment_provider_account")?
245    } else {
246        declared_fields(
247            schema,
248            "payment_provider_account",
249            &[
250                "app_id",
251                "id",
252                "tenant_id",
253                "owner_resource_type",
254                "owner_resource_id",
255                "provider_id",
256                "checkout_mode",
257                "connection_state",
258                "capability_state",
259                "onboarding_state",
260                "provider_account_id",
261                "provider_account_label",
262                "status_reason_code",
263                "status_reason_detail",
264                "charges_enabled",
265                "payouts_enabled",
266                "details_submitted",
267            ],
268        )?
269    };
270    let accounts = adapter.query(
271        config,
272        schema,
273        &Query {
274            entity: "payment_provider_account".to_owned(),
275            fields: account_fields,
276            filter: Some(scope.clone()),
277            order: Vec::new(),
278            limit: 2,
279            offset: 0,
280        },
281    )?;
282    if accounts.len() > 1 {
283        return Err(DatabaseError::Conflict);
284    }
285    let secret_fields = if actor_id.is_some() {
286        all_fields(schema, "payment_provider_secret")?
287    } else {
288        declared_fields(
289            schema,
290            "payment_provider_secret",
291            &[
292                "app_id",
293                "tenant_id",
294                "owner_resource_type",
295                "owner_resource_id",
296                "provider_id",
297                "secret_kind",
298                "ciphertext",
299                "revoked_at",
300            ],
301        )?
302    };
303    let mut secrets = adapter.query(
304        config,
305        schema,
306        &Query {
307            entity: "payment_provider_secret".to_owned(),
308            fields: secret_fields,
309            filter: Some(scope.clone()),
310            order: Vec::new(),
311            limit: 17,
312            offset: 0,
313        },
314    )?;
315    if secrets.len() > 16 {
316        return Err(DatabaseError::CapacityExceeded);
317    }
318    for row in secrets.iter_mut().filter(|_| actor_id.is_none()) {
319        let present = row
320            .get("ciphertext")
321            .and_then(Value::as_str)
322            .is_some_and(|text| !text.is_empty());
323        row.insert(
324            "ciphertext".to_owned(),
325            Value::String(if present { "present" } else { "" }.to_owned()),
326        );
327    }
328    let mut root = Map::new();
329    root.insert(
330        owner_section.to_owned(),
331        Value::Array(vec![Value::Object(owner)]),
332    );
333    if tenant_section != owner_section {
334        root.insert(
335            tenant_section.to_owned(),
336            Value::Array(vec![Value::Object(tenant)]),
337        );
338    }
339    root.insert(
340        "payment_provider_accounts".to_owned(),
341        Value::Array(accounts.into_iter().map(Value::Object).collect()),
342    );
343    root.insert(
344        "payment_provider_secrets".to_owned(),
345        Value::Array(secrets.into_iter().map(Value::Object).collect()),
346    );
347    if let Some(actor_id) = actor_id {
348        root.insert(
349            "_payment_scope".to_owned(),
350            serde_json::json!({
351                "app_id": schema.app_id, "tenant_id": tenant_id,
352                "owner_resource_type": owner_type, "owner_resource_id": owner_id,
353                "provider_id": provider_id, "actor_user_id": actor_id,
354            }),
355        );
356        if let Some(entity) = declaration.get("membership_entity").and_then(Value::as_str) {
357            let section = declared_text(declaration, "membership_state_section")?;
358            let user_field = declared_text(declaration, "membership_user_id_field")?;
359            let tenant_field = declared_text(declaration, "membership_tenant_id_field")?;
360            let status_field = declared_text(declaration, "membership_status_field")?;
361            let mut membership_fields = vec![user_field, tenant_field, status_field];
362            for key in ["membership_role_field", "membership_revoked_field"] {
363                if let Some(field) = declaration.get(key).and_then(Value::as_str) {
364                    membership_fields.push(field);
365                }
366            }
367            let rows = adapter.query(
368                config,
369                schema,
370                &Query {
371                    entity: entity.to_owned(),
372                    fields: declared_fields(schema, entity, &membership_fields)?,
373                    filter: Some(Predicate::All(vec![
374                        Predicate::Equal(user_field.to_owned(), Value::from(actor_id)),
375                        Predicate::Equal(tenant_field.to_owned(), Value::from(tenant_id)),
376                    ])),
377                    order: Vec::new(),
378                    limit: 2,
379                    offset: 0,
380                },
381            )?;
382            if rows.len() > 1 {
383                return Err(DatabaseError::Conflict);
384            }
385            root.insert(
386                section.to_owned(),
387                Value::Array(rows.into_iter().map(Value::Object).collect()),
388            );
389        }
390        let epoch = std::time::SystemTime::now()
391            .duration_since(std::time::UNIX_EPOCH)
392            .map_err(|_| DatabaseError::StorageFailure)?
393            .as_secs();
394        let now = i64::try_from(epoch)
395            .ok()
396            .and_then(|epoch| chrono::DateTime::<chrono::Utc>::from_timestamp(epoch, 0))
397            .ok_or(DatabaseError::StorageFailure)?
398            .to_rfc3339();
399        let nonces = adapter.query(
400            config,
401            schema,
402            &Query {
403                entity: "payment_provider_callback_nonce".to_owned(),
404                fields: all_fields(schema, "payment_provider_callback_nonce")?,
405                filter: Some(Predicate::All(vec![
406                    scope,
407                    Predicate::IsNull("claimed_at".to_owned()),
408                    Predicate::GreaterThan("expires_at".to_owned(), Value::String(now)),
409                ])),
410                order: Vec::new(),
411                limit: 257,
412                offset: 0,
413            },
414        )?;
415        if nonces.len() > 256 {
416            return Err(DatabaseError::CapacityExceeded);
417        }
418        root.insert(
419            "payment_provider_callback_nonces".to_owned(),
420            Value::Array(nonces.into_iter().map(Value::Object).collect()),
421        );
422        root.insert(
423            "payment_provider_audit_events".to_owned(),
424            Value::Array(Vec::new()),
425        );
426        // Legacy service constructors allocate provisional IDs from their view.
427        // A single indexed maximum per entity supplies the seed; the transaction
428        // below replaces provisional IDs with backend-generated identities.
429        for (section, entity) in PAYMENT_SECTIONS {
430            let latest = adapter.query(
431                config,
432                schema,
433                &Query {
434                    entity: (*entity).to_owned(),
435                    fields: vec!["id".to_owned()],
436                    filter: None,
437                    order: vec![SortKey {
438                        field: "id".to_owned(),
439                        descending: true,
440                    }],
441                    limit: 1,
442                    offset: 0,
443                },
444            )?;
445            let rows = root
446                .get_mut(*section)
447                .and_then(Value::as_array_mut)
448                .ok_or(DatabaseError::InvalidInput)?;
449            for row in latest {
450                if !rows
451                    .iter()
452                    .any(|existing| existing.get("id") == row.get("id"))
453                {
454                    rows.push(Value::Object(row));
455                }
456            }
457        }
458    }
459    Ok(Value::Object(root))
460}
461
462const PAYMENT_SECTIONS: &[(&str, &str)] = &[
463    ("payment_provider_secrets", "payment_provider_secret"),
464    ("payment_provider_accounts", "payment_provider_account"),
465    (
466        "payment_provider_callback_nonces",
467        "payment_provider_callback_nonce",
468    ),
469    (
470        "payment_provider_audit_events",
471        "payment_provider_audit_event",
472    ),
473];
474
475fn identity_reference<'a>(entity: &str, field: &'a crate::operations::Field) -> Option<&'a str> {
476    if entity == "payment_provider_account" && field.name == "secret_reference_id" {
477        Some("payment_provider_secret")
478    } else {
479        field
480            .references
481            .as_ref()
482            .filter(|(_, target)| target == "id")
483            .map(|(target, _)| target.as_str())
484    }
485}
486
487/// Commits changed payment rows atomically, with compare-and-write guards and
488/// transaction-local references for identities generated by any backend.
489pub fn apply_payment_lifecycle_delta(
490    adapter: &dyn DatabaseOperations,
491    config: &DbBackendConfig,
492    schema: &AppSchema,
493    declaration: &Value,
494    actor_id: i64,
495    before: &Value,
496    after: &Value,
497) -> Result<Value, DatabaseError> {
498    let scope = before
499        .get("_payment_scope")
500        .and_then(Value::as_object)
501        .ok_or(DatabaseError::InvalidInput)?;
502    if scope.get("app_id").and_then(Value::as_str) != Some(schema.app_id.as_str())
503        || scope.get("actor_user_id").and_then(Value::as_i64) != Some(actor_id)
504        || before.get("_payment_scope") != after.get("_payment_scope")
505    {
506        return Err(DatabaseError::InvalidInput);
507    }
508    let mut steps = vec![
509        Mutation::RequireCount {
510            entity: "user_account".to_owned(),
511            filter: Predicate::All(vec![
512                Predicate::Equal("id".to_owned(), Value::from(actor_id)),
513                Predicate::Equal("is_active".to_owned(), Value::Bool(true)),
514            ]),
515            minimum: 1,
516            maximum: Some(1),
517        },
518        payment_authorization_guard(declaration, actor_id, before)?,
519    ];
520    let mut inserted = BTreeMap::new();
521    for &(section, entity_name) in PAYMENT_SECTIONS {
522        let entity = schema
523            .entities
524            .iter()
525            .find(|entity| entity.name == entity_name)
526            .ok_or(DatabaseError::InvalidContract)?;
527        let old_rows = before
528            .get(section)
529            .and_then(Value::as_array)
530            .ok_or(DatabaseError::InvalidInput)?;
531        let new_rows = after
532            .get(section)
533            .and_then(Value::as_array)
534            .ok_or(DatabaseError::InvalidInput)?;
535        let mut old_by_id = BTreeMap::new();
536        for row in old_rows {
537            let id = row
538                .get("id")
539                .and_then(Value::as_i64)
540                .filter(|id| *id > 0)
541                .ok_or(DatabaseError::InvalidInput)?;
542            if old_by_id.insert(id, row).is_some() {
543                return Err(DatabaseError::InvalidInput);
544            }
545        }
546        let mut seen = std::collections::BTreeSet::new();
547        for row in new_rows {
548            let id = row
549                .get("id")
550                .and_then(Value::as_i64)
551                .filter(|id| *id > 0)
552                .ok_or(DatabaseError::InvalidInput)?;
553            if !seen.insert(id) {
554                return Err(DatabaseError::InvalidInput);
555            }
556            let old = old_by_id.remove(&id);
557            if old == Some(row) {
558                continue;
559            }
560            if scope
561                .iter()
562                .filter(|(name, _)| name.as_str() != "actor_user_id")
563                .any(|(name, expected)| row.get(name) != Some(expected))
564            {
565                return Err(DatabaseError::AccessDenied);
566            }
567            let fields = row.as_object().ok_or(DatabaseError::InvalidInput)?;
568            let mut values = BTreeMap::new();
569            for (name, value) in fields {
570                if name == "id" || old.is_some_and(|old| old.get(name) == Some(value)) {
571                    continue;
572                }
573                let field = entity
574                    .fields
575                    .iter()
576                    .find(|field| field.name == *name)
577                    .ok_or(DatabaseError::InvalidContract)?;
578                let write = if let Some(target_entity) = identity_reference(entity_name, field)
579                    && let Some(provisional) = value.as_i64()
580                    && let Some(step) = inserted.get(&(target_entity.to_owned(), provisional))
581                {
582                    WriteValue::GeneratedId(*step)
583                } else {
584                    let value = if field.nullable
585                        && field.kind == crate::operations::FieldType::Timestamp
586                        && value.as_str() == Some("")
587                    {
588                        Value::Null
589                    } else {
590                        value.clone()
591                    };
592                    WriteValue::Literal(value)
593                };
594                values.insert(name.clone(), write);
595            }
596            if let Some(old) = old {
597                let mut guard = vec![Predicate::Equal("id".to_owned(), Value::from(id))];
598                for name in values.keys() {
599                    let previous = old.get(name).ok_or(DatabaseError::InvalidInput)?;
600                    guard.push(if previous.is_null() {
601                        Predicate::IsNull(name.clone())
602                    } else {
603                        Predicate::Equal(name.clone(), previous.clone())
604                    });
605                }
606                if !values.is_empty() {
607                    steps.push(Mutation::Update {
608                        entity: entity_name.to_owned(),
609                        filter: Predicate::All(guard),
610                        values,
611                        expected_rows: Some(1),
612                    });
613                }
614            } else {
615                inserted.insert((entity_name.to_owned(), id), steps.len());
616                steps.push(Mutation::Insert {
617                    entity: entity_name.to_owned(),
618                    values,
619                });
620            }
621        }
622        if !old_by_id.is_empty() {
623            return Err(DatabaseError::InvalidInput);
624        }
625    }
626    if steps.len() == 2 {
627        return Ok(after.clone());
628    }
629    let result = adapter.transact(
630        config,
631        schema,
632        &Transaction {
633            steps,
634            idempotency_key: None,
635        },
636    )?;
637    let mut corrected = after.clone();
638    for &(section, entity_name) in PAYMENT_SECTIONS {
639        let entity = schema
640            .entities
641            .iter()
642            .find(|entity| entity.name == entity_name)
643            .ok_or(DatabaseError::InvalidContract)?;
644        let rows = corrected
645            .get_mut(section)
646            .and_then(Value::as_array_mut)
647            .ok_or(DatabaseError::InvalidInput)?;
648        for row in rows {
649            let fields = row.as_object_mut().ok_or(DatabaseError::InvalidInput)?;
650            for field in &entity.fields {
651                let target = if field.name == "id" {
652                    Some(entity_name)
653                } else {
654                    identity_reference(entity_name, field)
655                };
656                if let Some(target) = target
657                    && let Some(provisional) = fields.get(&field.name).and_then(Value::as_i64)
658                    && let Some(step) = inserted.get(&(target.to_owned(), provisional))
659                {
660                    let actual = result
661                        .generated_ids
662                        .get(*step)
663                        .copied()
664                        .flatten()
665                        .ok_or(DatabaseError::StorageFailure)?;
666                    fields.insert(field.name.clone(), Value::from(actual));
667                }
668            }
669        }
670    }
671    Ok(corrected)
672}
673
674fn payment_authorization_guard(
675    declaration: &Value,
676    actor_id: i64,
677    state: &Value,
678) -> Result<Mutation, DatabaseError> {
679    if actor_id <= 0 {
680        return Err(DatabaseError::InvalidInput);
681    }
682    for (entity_key, section_key, user_key, id_key) in [
683        (
684            "owner_entity",
685            "owner_state_section",
686            "owner_user_id_field",
687            "owner_id_field",
688        ),
689        (
690            "tenant_entity",
691            "tenant_state_section",
692            "tenant_owner_user_id_field",
693            "tenant_id_field",
694        ),
695    ] {
696        let Some(user_field) = declaration.get(user_key).and_then(Value::as_str) else {
697            continue;
698        };
699        let entity = declared_text(declaration, entity_key)?;
700        let section = declared_text(declaration, section_key)?;
701        let id_field = declared_text(declaration, id_key)?;
702        if let Some(row) = state
703            .get(section)
704            .and_then(Value::as_array)
705            .and_then(|rows| rows.first())
706            && row.get(user_field).and_then(Value::as_i64) == Some(actor_id)
707        {
708            let identity = row
709                .get(id_field)
710                .cloned()
711                .ok_or(DatabaseError::InvalidInput)?;
712            return Ok(Mutation::RequireCount {
713                entity: entity.to_owned(),
714                filter: Predicate::All(vec![
715                    Predicate::Equal(id_field.to_owned(), identity),
716                    Predicate::Equal(user_field.to_owned(), Value::from(actor_id)),
717                ]),
718                minimum: 1,
719                maximum: Some(1),
720            });
721        }
722    }
723    let entity = declared_text(declaration, "membership_entity")?;
724    let section = declared_text(declaration, "membership_state_section")?;
725    let user_field = declared_text(declaration, "membership_user_id_field")?;
726    let tenant_field = declared_text(declaration, "membership_tenant_id_field")?;
727    let status_field = declared_text(declaration, "membership_status_field")?;
728    let row = state
729        .get(section)
730        .and_then(Value::as_array)
731        .and_then(|rows| rows.first())
732        .ok_or(DatabaseError::AccessDenied)?;
733    if row.get(user_field).and_then(Value::as_i64) != Some(actor_id)
734        || row.get(status_field).and_then(Value::as_str) != Some("active")
735    {
736        return Err(DatabaseError::AccessDenied);
737    }
738    let mut filters = vec![
739        Predicate::Equal(user_field.to_owned(), Value::from(actor_id)),
740        Predicate::Equal(
741            tenant_field.to_owned(),
742            row.get(tenant_field)
743                .cloned()
744                .ok_or(DatabaseError::InvalidInput)?,
745        ),
746        Predicate::Equal(status_field.to_owned(), Value::String("active".to_owned())),
747    ];
748    if let Some(field) = declaration
749        .get("membership_role_field")
750        .and_then(Value::as_str)
751    {
752        let role = row
753            .get(field)
754            .and_then(Value::as_str)
755            .ok_or(DatabaseError::AccessDenied)?;
756        let allowed = declaration
757            .get("membership_allowed_roles")
758            .and_then(Value::as_array)
759            .filter(|roles| !roles.is_empty() && roles.len() <= 32)
760            .ok_or(DatabaseError::InvalidContract)?;
761        if !allowed.iter().any(|allowed| allowed.as_str() == Some(role)) {
762            return Err(DatabaseError::AccessDenied);
763        }
764        filters.push(Predicate::Equal(
765            field.to_owned(),
766            Value::String(role.to_owned()),
767        ));
768    }
769    if let Some(field) = declaration
770        .get("membership_revoked_field")
771        .and_then(Value::as_str)
772    {
773        if row.get(field) != Some(&Value::Null) {
774            return Err(DatabaseError::AccessDenied);
775        }
776        filters.push(Predicate::IsNull(field.to_owned()));
777    }
778    Ok(Mutation::RequireCount {
779        entity: entity.to_owned(),
780        filter: Predicate::All(filters),
781        minimum: 1,
782        maximum: Some(1),
783    })
784}