Skip to content

Commit e8342ca

Browse files
committed
chore: add update-profile use case
1 parent 2e7182d commit e8342ca

File tree

8 files changed

+133
-13
lines changed

8 files changed

+133
-13
lines changed

proto/api/bria.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ option go_package = "github.com/GaloyMoney/terraform-provider-bria/client/proto/
88

99
service BriaService {
1010
rpc CreateProfile (CreateProfileRequest) returns (CreateProfileResponse) {}
11+
rpc UpdateProfile (UpdateProfileRequest) returns (UpdateProfileResponse) {}
1112
rpc ListProfiles (ListProfilesRequest) returns (ListProfilesResponse) {}
1213
rpc CreateProfileApiKey (CreateProfileApiKeyRequest) returns (CreateProfileApiKeyResponse) {}
1314

@@ -60,6 +61,13 @@ message CreateProfileResponse {
6061
string id = 1;
6162
}
6263

64+
message UpdateProfileRequest {
65+
string id = 1;
66+
optional SpendingPolicy spending_policy = 2;
67+
}
68+
69+
message UpdateProfileResponse {}
70+
6371
message CreateProfileApiKeyRequest {
6472
string profile_name = 1;
6573
}

src/api/server/convert.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,9 @@ impl From<ApplicationError> for tonic::Status {
644644
ApplicationError::ProfileError(ProfileError::ProfileNameNotFound(_)) => {
645645
tonic::Status::not_found(err.to_string())
646646
}
647+
ApplicationError::ProfileError(ProfileError::ProfileIdNotFound(_)) => {
648+
tonic::Status::not_found(err.to_string())
649+
}
647650
ApplicationError::PayoutError(PayoutError::PayoutIdNotFound(_)) => {
648651
tonic::Status::not_found(err.to_string())
649652
}

src/api/server/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,36 @@ impl BriaService for Bria {
5757
.await
5858
}
5959

60+
#[instrument(name = "bria.update_profile", skip_all, fields(error, error.level, error.message), err)]
61+
async fn update_profile(
62+
&self,
63+
request: Request<UpdateProfileRequest>,
64+
) -> Result<Response<UpdateProfileResponse>, Status> {
65+
crate::tracing::record_error(|| async move {
66+
extract_tracing(&request);
67+
68+
let key = extract_api_token(&request)?;
69+
let profile = self.app.authenticate(key).await?;
70+
let request = request.into_inner();
71+
let spending_policy = request
72+
.spending_policy
73+
.map(|policy| profile::SpendingPolicy::try_from((policy, self.app.network())))
74+
.transpose()?;
75+
self.app
76+
.update_profile(
77+
&profile,
78+
request
79+
.id
80+
.parse()
81+
.map_err(ApplicationError::CouldNotParseIncomingUuid)?,
82+
spending_policy,
83+
)
84+
.await?;
85+
Ok(Response::new(UpdateProfileResponse {}))
86+
})
87+
.await
88+
}
89+
6090
#[instrument(name = "bria.list_profiles", skip_all, fields(error, error.level, error.message), err)]
6191
async fn list_profiles(
6292
&self,

src/app/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,24 @@ impl App {
153153
Ok(new_profile)
154154
}
155155

156+
#[instrument(name = "app.update_profile", skip(self), err)]
157+
pub async fn update_profile(
158+
&self,
159+
profile: &Profile,
160+
profile_id: ProfileId,
161+
spending_policy: Option<SpendingPolicy>,
162+
) -> Result<(), ApplicationError> {
163+
let mut target_profile = self
164+
.profiles
165+
.find_by_id(profile.account_id, profile_id)
166+
.await?;
167+
target_profile.update_spending_policy(spending_policy);
168+
let mut tx = self.pool.begin().await?;
169+
self.profiles.update(&mut tx, target_profile).await?;
170+
tx.commit().await?;
171+
Ok(())
172+
}
173+
156174
#[instrument(name = "app.list_profiles", skip(self), err)]
157175
pub async fn list_profiles(&self, profile: &Profile) -> Result<Vec<Profile>, ApplicationError> {
158176
let profiles = self.profiles.list_for_account(profile.account_id).await?;

src/profile/entity.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub enum ProfileEvent {
1616
SpendingPolicyUpdated {
1717
spending_policy: SpendingPolicy,
1818
},
19+
SpendingPolicyRemoved {},
1920
}
2021

2122
#[derive(Debug, Builder)]
@@ -24,11 +25,26 @@ pub struct Profile {
2425
pub id: ProfileId,
2526
pub account_id: AccountId,
2627
pub name: String,
27-
#[builder(default, setter(strip_option))]
28+
#[builder(default)]
2829
pub spending_policy: Option<SpendingPolicy>,
30+
31+
pub(super) events: EntityEvents<ProfileEvent>,
2932
}
3033

3134
impl Profile {
35+
pub fn update_spending_policy(&mut self, policy: Option<SpendingPolicy>) {
36+
if self.spending_policy != policy {
37+
self.spending_policy = policy.clone();
38+
if let Some(policy) = policy {
39+
self.events.push(ProfileEvent::SpendingPolicyUpdated {
40+
spending_policy: policy,
41+
});
42+
} else {
43+
self.events.push(ProfileEvent::SpendingPolicyRemoved {});
44+
}
45+
}
46+
}
47+
3248
pub fn is_destination_allowed(&self, destination: &PayoutDestination) -> bool {
3349
self.spending_policy
3450
.as_ref()
@@ -44,7 +60,7 @@ impl Profile {
4460
}
4561
}
4662

47-
#[derive(Debug, Clone, Serialize, Deserialize)]
63+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4864
pub struct SpendingPolicy {
4965
pub allowed_payout_addresses: Vec<Address>,
5066
pub maximum_payout: Option<Satoshis>,
@@ -109,19 +125,20 @@ impl TryFrom<EntityEvents<ProfileEvent>> for Profile {
109125

110126
fn try_from(events: EntityEvents<ProfileEvent>) -> Result<Self, Self::Error> {
111127
let mut builder = ProfileBuilder::default();
112-
for event in events.into_iter() {
128+
for event in events.iter() {
113129
match event {
114130
ProfileEvent::Initialized { id, account_id } => {
115-
builder = builder.id(id).account_id(account_id)
131+
builder = builder.id(*id).account_id(*account_id)
116132
}
117133
ProfileEvent::NameUpdated { name } => {
118-
builder = builder.name(name);
134+
builder = builder.name(name.clone());
119135
}
120136
ProfileEvent::SpendingPolicyUpdated { spending_policy } => {
121-
builder = builder.spending_policy(spending_policy);
137+
builder = builder.spending_policy(Some(spending_policy.clone()));
122138
}
139+
ProfileEvent::SpendingPolicyRemoved {} => builder = builder.spending_policy(None),
123140
}
124141
}
125-
builder.build()
142+
builder.events(events).build()
126143
}
127144
}

src/profile/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
use thiserror::Error;
22

3+
use crate::primitives::ProfileId;
4+
35
#[derive(Error, Debug)]
46
pub enum ProfileError {
57
#[error("ProfileError - Api key does not exist")]
68
ProfileKeyNotFound,
79
#[error("ProfileError - Could not find profile with name: {0}")]
810
ProfileNameNotFound(String),
11+
#[error("ProfileError - Could not find profile with id: {0}")]
12+
ProfileIdNotFound(ProfileId),
913
#[error("ProfileError - Sqlx: {0}")]
1014
Sqlx(#[from] sqlx::Error),
1115
#[error("ProfileError - EntityError: {0}")]

src/profile/repo.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,34 @@ impl Profiles {
7070
Ok(profiles)
7171
}
7272

73+
pub async fn find_by_id(
74+
&self,
75+
account_id: AccountId,
76+
id: ProfileId,
77+
) -> Result<Profile, ProfileError> {
78+
let rows = sqlx::query!(
79+
r#"SELECT p.id, e.sequence, e.event_type, e.event
80+
FROM bria_profiles p
81+
JOIN bria_profile_events e ON p.id = e.id
82+
WHERE p.account_id = $1 AND p.id = $2
83+
ORDER BY p.id, sequence"#,
84+
account_id as AccountId,
85+
id as ProfileId
86+
)
87+
.fetch_all(&self.pool)
88+
.await?;
89+
90+
if !rows.is_empty() {
91+
let mut events = EntityEvents::new();
92+
for row in rows {
93+
events.load_event(row.sequence as usize, row.event)?;
94+
}
95+
Ok(Profile::try_from(events)?)
96+
} else {
97+
Err(ProfileError::ProfileIdNotFound(id))
98+
}
99+
}
100+
73101
pub async fn find_by_name(
74102
&self,
75103
account_id: AccountId,
@@ -157,4 +185,21 @@ impl Profiles {
157185
Err(ProfileError::ProfileKeyNotFound)
158186
}
159187
}
188+
189+
pub async fn update(
190+
&self,
191+
tx: &mut Transaction<'_, Postgres>,
192+
profile: Profile,
193+
) -> Result<(), ProfileError> {
194+
if !profile.events.is_dirty() {
195+
return Ok(());
196+
}
197+
EntityEvents::<ProfileEvent>::persist(
198+
"bria_profile_events",
199+
tx,
200+
profile.events.new_serialized_events(profile.id),
201+
)
202+
.await?;
203+
Ok(())
204+
}
160205
}

tests/helpers.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,7 @@ pub async fn create_test_account(pool: &sqlx::PgPool) -> anyhow::Result<Profile>
3535
let app = AdminApp::new(pool.clone(), bitcoin::Network::Regtest);
3636

3737
let profile_key = app.create_account(name.clone()).await?;
38-
Ok(Profile {
39-
id: profile_key.profile_id,
40-
account_id: profile_key.account_id,
41-
name,
42-
spending_policy: None,
43-
})
38+
Ok(Profiles::new(pool).find_by_key(&profile_key.key).await?)
4439
}
4540

4641
pub async fn bitcoind_client() -> anyhow::Result<bitcoincore_rpc::Client> {

0 commit comments

Comments
 (0)