Skip to content

📞 [src] implement contact updates#29

Merged
chicks-net merged 5 commits intomainfrom
chicks/2026-02-26-contact-updates
Feb 27, 2026
Merged

📞 [src] implement contact updates#29
chicks-net merged 5 commits intomainfrom
chicks/2026-02-26-contact-updates

Conversation

@chicks-net
Copy link
Copy Markdown
Member

@chicks-net chicks-net commented Feb 27, 2026

Done

  • 📞 [src] implement contact updates
  • switch money to reals
  • formatting
  • create rust-toolchain.toml to stabilize CI
  • fixes for clippy

Meta

(Automated in .just/gh-process.just.)

Copilot AI review requested due to automatic review settings February 27, 2026 03:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements domain contact updates end-to-end: adds OpenSRS set_contact request/response support, exposes new client/types, and introduces a CLI command to update OpenSRS + local DB contacts with audit logging. Also updates DB documentation to use REAL for money fields.

Changes:

  • Add OpenSRS set_contact XML serialization/deserialization plus a client method to update contacts.
  • Add new OpenSRS contact update types/conversions and a CLI subcommand to perform updates and persist/audit locally.
  • Update docs to represent monetary columns as REAL instead of DECIMAL.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/opensrs/xml.rs Adds set_contact XML serializer/parser; updates get_domain all_info parsing to extract contact fields.
src/opensrs/types.rs Introduces SetContact* request/response types and contact conversion helpers.
src/opensrs/mod.rs Re-exports newly added OpenSRS contact update types.
src/opensrs/client.rs Adds update_domain_contacts() that calls OpenSRS set_contact.
src/main.rs Adds Contacts CLI command to update OpenSRS then update/relink contacts in the local DB and write an audit entry.
src/lib.rs Re-exports new OpenSRS contact types for external callers.
src/db/contacts.rs Adds UpdateStrategy and update_contact() to update-in-place vs create-new-and-relink; makes ContactInfo serializable for audits.
src/db/audit.rs Adds audit helpers for logging contact updates with old/new JSON values.
docs/schema.sql Updates schema docs to store money fields as REAL.
docs/erd.md Updates ERD docs to reflect REAL money fields.
docs/database-schema.md Updates schema docs and type-affinity notes to reflect REAL money fields.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +18
account_balance REAL NOT NULL DEFAULT 0.00,
credit_limit REAL NOT NULL DEFAULT 0.00,
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching money fields to SQLite REAL (floating point) can introduce rounding issues (e.g., sums and comparisons may not be exact). If these columns represent currency amounts, consider documenting expected rounding behavior and/or using an integer minor-unit representation (cents) to keep calculations exact.

Suggested change
account_balance REAL NOT NULL DEFAULT 0.00,
credit_limit REAL NOT NULL DEFAULT 0.00,
account_balance INTEGER NOT NULL DEFAULT 0, -- stored in minor currency units (e.g., cents)
credit_limit INTEGER NOT NULL DEFAULT 0, -- stored in minor currency units (e.g., cents)

Copilot uses AI. Check for mistakes.
Comment on lines +416 to 418
- `REAL`: For decimal/money values (floating point)
- `BOOLEAN`: Stored as INTEGER (0 = false, 1 = true)
- `DATETIME`: Stored as TEXT in ISO 8601 format
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documenting money as SQLite REAL (floating point) can mislead readers into assuming exact currency arithmetic; REAL will introduce rounding error in many cases. If these fields are currency, consider recommending integer minor units (cents) or clearly stating required rounding/formatting rules in the application layer.

Suggested change
- `REAL`: For decimal/money values (floating point)
- `BOOLEAN`: Stored as INTEGER (0 = false, 1 = true)
- `DATETIME`: Stored as TEXT in ISO 8601 format
- `REAL`: For non-monetary approximate decimal values (floating point, may introduce rounding error)
- `BOOLEAN`: Stored as INTEGER (0 = false, 1 = true)
- `DATETIME`: Stored as TEXT in ISO 8601 format
- Monetary values: Prefer fixed-point representation, for example storing minor units as `INTEGER` (cents) or using `NUMERIC` with a clearly documented scale and application-level rounding/formatting.

Copilot uses AI. Check for mistakes.
Comment on lines +285 to +289
<dt_assoc>
<item key="domain">"#,
);
xml.push_str(&request.attributes.domain);
xml.push_str(
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serialize_set_contact_request builds XML by concatenating unescaped user-supplied strings (domain and all contact fields). If any field contains XML metacharacters like '&' or '<', the request becomes invalid XML and can enable XML injection. Use a proper XML writer (e.g., quick_xml Writer/BytesText) or at minimum escape text node contents before appending.

Copilot uses AI. Check for mistakes.
Comment on lines +777 to +781
} else if name == "item" {
// Check if we're exiting a contact type
if let (Some(contact_type), Some(contact)) =
(current_contact_type.take(), current_contact.take())
{
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commits the current contact on every end tag. Because the OpenSRS contact structure nests many elements inside the contact, the contact will be finalized after the first field item and the remaining fields will be dropped. Track nesting depth / a key stack so you only commit when the closing corresponds to the owner/admin/billing/tech container item.

Copilot uses AI. Check for mistakes.
Comment on lines +257 to +258
/// Serialize set_contact request to OpenSRS XML format
pub fn serialize_set_contact_request(request: &SetContactRequest) -> Result<String> {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New contact update serialization/deserialization and the modified contact parsing logic are not covered by tests in this module. Adding unit tests that (1) serialize a SetContactRequest containing fields with optional values and XML metacharacters and (2) deserialize a representative get_domain all_info response containing a fully-populated contact_set would help prevent regressions.

Copilot uses AI. Check for mistakes.
impl From<&ContactInfo> for crate::db::contacts::ContactInfo {
fn from(contact: &ContactInfo) -> Self {
Self {
contact_type: String::new(),
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This From<&ContactInfo> conversion produces a db::contacts::ContactInfo with contact_type set to an empty string, which is not a valid/meaningful value and can violate DB constraints if used. Prefer a conversion API that requires the contact role/type as an input (e.g., a helper fn taking a role, or a TryFrom that validates), rather than implementing From with an invalid default.

Suggested change
contact_type: String::new(),
// Use a nonempty default contact type to avoid invalid/meaningless values.
contact_type: "unknown".to_string(),

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +210
let _ = DomainContacts::delete_many()
.filter(domain_contacts::Column::DomainId.eq(did))
.filter(domain_contacts::Column::ContactRole.eq(&contact_info.contact_type))
.exec(db)
.await;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_contact ignores the result of deleting the existing (domain_id, contact_role) link. If the delete fails or deletes 0 rows, the subsequent insert can fail due to the (domain_id, contact_role) primary key, and the underlying cause will be obscured. Propagate the delete result (and consider asserting that exactly 1 row was deleted) before inserting the new link.

Suggested change
let _ = DomainContacts::delete_many()
.filter(domain_contacts::Column::DomainId.eq(did))
.filter(domain_contacts::Column::ContactRole.eq(&contact_info.contact_type))
.exec(db)
.await;
DomainContacts::delete_many()
.filter(domain_contacts::Column::DomainId.eq(did))
.filter(domain_contacts::Column::ContactRole.eq(&contact_info.contact_type))
.exec(db)
.await?;

Copilot uses AI. Check for mistakes.
src/db/audit.rs Outdated
Comment on lines +111 to +116
let old_json = old_values.map(|c| {
serde_json::to_string(c).unwrap_or_default()
});
let new_json = new_values.map(|c| {
serde_json::to_string(c).unwrap_or_default()
});
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log_contact_update silently drops audit JSON if serde_json::to_string fails (unwrap_or_default produces an empty string). For audit logs, silently losing old values makes troubleshooting harder. Consider handling serialization failure explicitly (e.g., store an error marker string, or return an error) instead of defaulting to empty.

Suggested change
let old_json = old_values.map(|c| {
serde_json::to_string(c).unwrap_or_default()
});
let new_json = new_values.map(|c| {
serde_json::to_string(c).unwrap_or_default()
});
let old_json = match old_values {
Some(c) => match serde_json::to_string(c) {
Ok(s) => Some(s),
Err(_) => Some(r#"{"error":"failed to serialize old contact values"}"#.to_string()),
},
None => None,
};
let new_json = match new_values {
Some(c) => match serde_json::to_string(c) {
Ok(s) => Some(s),
Err(_) => Some(r#"{"error":"failed to serialize new contact values"}"#.to_string()),
},
None => None,
};

Copilot uses AI. Check for mistakes.
@chicks-net chicks-net merged commit d59997c into main Feb 27, 2026
15 of 16 checks passed
@chicks-net chicks-net deleted the chicks/2026-02-26-contact-updates branch February 27, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants