1use async_graphql::{dataloader::*, types::connection::*, *};
2use cala_ledger::{balance::AccountBalance, primitives::*, tx_template::NewParamDefinition};
3
4use crate::{app::CalaApp, extension::*};
5
6use super::{
7 account::*, account_set::*, balance::*, journal::*, loader::*, primitives::*, transaction::*,
8 tx_template::*, velocity::*,
9};
10
11#[derive(Default)]
12pub struct CoreQuery<E: QueryExtensionMarker> {
13 _phantom: std::marker::PhantomData<E>,
14}
15
16#[Object(name = "Query")]
17impl<E: QueryExtensionMarker> CoreQuery<E> {
18 #[graphql(flatten)]
19 async fn extension(&self) -> E {
20 E::default()
21 }
22
23 async fn account(&self, ctx: &Context<'_>, id: UUID) -> async_graphql::Result<Option<Account>> {
24 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
25 Ok(loader.load_one(AccountId::from(id)).await?)
26 }
27
28 async fn account_by_external_id(
29 &self,
30 ctx: &Context<'_>,
31 external_id: String,
32 ) -> async_graphql::Result<Option<Account>> {
33 let app = ctx.data_unchecked::<CalaApp>();
34 match app
35 .ledger()
36 .accounts()
37 .find_by_external_id(external_id)
38 .await
39 {
40 Ok(account) => Ok(Some(account.into())),
41 Err(cala_ledger::account::error::AccountError::CouldNotFindByExternalId(_)) => Ok(None),
42 Err(err) => Err(err.into()),
43 }
44 }
45
46 async fn account_by_code(
47 &self,
48 ctx: &Context<'_>,
49 code: String,
50 ) -> async_graphql::Result<Option<Account>> {
51 let app = ctx.data_unchecked::<CalaApp>();
52 match app.ledger().accounts().find_by_code(code).await {
53 Ok(account) => Ok(Some(account.into())),
54 Err(cala_ledger::account::error::AccountError::CouldNotFindByCode(_)) => Ok(None),
55 Err(err) => Err(err.into()),
56 }
57 }
58
59 async fn accounts(
60 &self,
61 ctx: &Context<'_>,
62 first: i32,
63 after: Option<String>,
64 ) -> Result<Connection<AccountsByNameCursor, Account, EmptyFields, EmptyFields>> {
65 let app = ctx.data_unchecked::<CalaApp>();
66 query(
67 after,
68 None,
69 Some(first),
70 None,
71 |after, _, first, _| async move {
72 let first = first.expect("First always exists");
73 let result = app
74 .ledger()
75 .accounts()
76 .list(cala_ledger::es_entity::PaginatedQueryArgs { first, after })
77 .await?;
78 let mut connection = Connection::new(false, result.has_next_page);
79 connection
80 .edges
81 .extend(result.entities.into_iter().map(|entity| {
82 let cursor = AccountsByNameCursor::from(&entity);
83 Edge::new(cursor, Account::from(entity))
84 }));
85 Ok::<_, async_graphql::Error>(connection)
86 },
87 )
88 .await
89 }
90
91 async fn account_set(
92 &self,
93 ctx: &Context<'_>,
94 id: UUID,
95 ) -> async_graphql::Result<Option<AccountSet>> {
96 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
97 Ok(loader.load_one(AccountSetId::from(id)).await?)
98 }
99
100 async fn journal(&self, ctx: &Context<'_>, id: UUID) -> async_graphql::Result<Option<Journal>> {
101 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
102 Ok(loader.load_one(JournalId::from(id)).await?)
103 }
104
105 async fn balance(
106 &self,
107 ctx: &Context<'_>,
108 journal_id: UUID,
109 account_id: UUID,
110 currency: CurrencyCode,
111 ) -> async_graphql::Result<Option<Balance>> {
112 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
113 let journal_id = JournalId::from(journal_id);
114 let account_id = AccountId::from(account_id);
115 let currency = Currency::from(currency);
116 let balance: Option<AccountBalance> =
117 loader.load_one((journal_id, account_id, currency)).await?;
118 Ok(balance.map(Balance::from))
119 }
120
121 async fn transaction(
122 &self,
123 ctx: &Context<'_>,
124 id: UUID,
125 ) -> async_graphql::Result<Option<Transaction>> {
126 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
127 Ok(loader.load_one(TransactionId::from(id)).await?)
128 }
129
130 async fn transaction_by_external_id(
131 &self,
132 ctx: &Context<'_>,
133 external_id: String,
134 ) -> async_graphql::Result<Option<Transaction>> {
135 let app = ctx.data_unchecked::<CalaApp>();
136 match app
137 .ledger()
138 .transactions()
139 .find_by_external_id(external_id)
140 .await
141 {
142 Ok(transaction) => Ok(Some(transaction.into())),
143 Err(cala_ledger::transaction::error::TransactionError::CouldNotFindByExternalId(_)) => {
144 Ok(None)
145 }
146 Err(err) => Err(err.into()),
147 }
148 }
149
150 async fn tx_template(
151 &self,
152 ctx: &Context<'_>,
153 id: UUID,
154 ) -> async_graphql::Result<Option<TxTemplate>> {
155 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
156 Ok(loader.load_one(TxTemplateId::from(id)).await?)
157 }
158
159 async fn tx_template_by_code(
160 &self,
161 ctx: &Context<'_>,
162 code: String,
163 ) -> async_graphql::Result<Option<TxTemplate>> {
164 let app = ctx.data_unchecked::<CalaApp>();
165 match app.ledger().tx_templates().find_by_code(code).await {
166 Ok(tx_template) => Ok(Some(tx_template.into())),
167 Err(cala_ledger::tx_template::error::TxTemplateError::CouldNotFindByCode(_)) => {
168 Ok(None)
169 }
170 Err(err) => Err(err.into()),
171 }
172 }
173
174 async fn velocity_limit(
175 &self,
176 ctx: &Context<'_>,
177 id: UUID,
178 ) -> async_graphql::Result<Option<VelocityLimit>> {
179 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
180 Ok(loader.load_one(VelocityLimitId::from(id)).await?)
181 }
182
183 async fn velocity_control(
184 &self,
185 ctx: &Context<'_>,
186 id: UUID,
187 ) -> async_graphql::Result<Option<VelocityControl>> {
188 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
189 Ok(loader.load_one(VelocityControlId::from(id)).await?)
190 }
191}
192
193#[derive(Default)]
194pub struct CoreMutation<E: MutationExtensionMarker> {
195 _phantom: std::marker::PhantomData<E>,
196}
197
198#[Object(name = "Mutation")]
199impl<E: MutationExtensionMarker> CoreMutation<E> {
200 #[graphql(flatten)]
201 async fn extension(&self) -> E {
202 E::default()
203 }
204
205 async fn account_create(
206 &self,
207 ctx: &Context<'_>,
208 input: AccountCreateInput,
209 ) -> Result<AccountCreatePayload> {
210 let app = ctx.data_unchecked::<CalaApp>();
211 let mut op = ctx
212 .data_unchecked::<DbOp>()
213 .try_lock()
214 .expect("Lock held concurrently");
215 let mut builder = cala_ledger::account::NewAccount::builder();
216 builder
217 .id(input.account_id)
218 .name(input.name)
219 .code(input.code)
220 .normal_balance_type(input.normal_balance_type)
221 .status(input.status);
222
223 if let Some(external_id) = input.external_id {
224 builder.external_id(external_id);
225 }
226 if let Some(description) = input.description {
227 builder.description(description);
228 }
229 if let Some(metadata) = input.metadata {
230 builder.metadata(metadata)?;
231 }
232 let account = app
233 .ledger()
234 .accounts()
235 .create_in_op(&mut *op, builder.build()?)
236 .await?;
237
238 if let Some(account_set_ids) = input.account_set_ids {
239 for id in account_set_ids {
240 app.ledger()
241 .account_sets()
242 .add_member_in_op(&mut *op, AccountSetId::from(id), account.id())
243 .await?;
244 }
245 }
246
247 Ok(account.into())
248 }
249
250 async fn account_update(
251 &self,
252 ctx: &Context<'_>,
253 id: UUID,
254 input: AccountUpdateInput,
255 ) -> Result<AccountUpdatePayload> {
256 let app = ctx.data_unchecked::<CalaApp>();
257 let mut op = ctx
258 .data_unchecked::<DbOp>()
259 .try_lock()
260 .expect("Lock held concurrently");
261
262 let mut builder = cala_ledger::account::AccountUpdate::default();
263 if let Some(name) = input.name {
264 builder.name(name);
265 }
266 if let Some(code) = input.code {
267 builder.code(code);
268 }
269 if let Some(normal_balance_type) = input.normal_balance_type {
270 builder.normal_balance_type(normal_balance_type);
271 }
272 if let Some(status) = input.status {
273 builder.status(status);
274 }
275 if let Some(external_id) = input.external_id {
276 builder.external_id(external_id);
277 }
278 if let Some(description) = input.description {
279 builder.description(description);
280 }
281 if let Some(metadata) = input.metadata {
282 builder.metadata(metadata)?;
283 }
284
285 let mut account = app.ledger().accounts().find(AccountId::from(id)).await?;
286 if account.update(builder).did_execute() {
287 app.ledger()
288 .accounts()
289 .persist_in_op(&mut *op, &mut account)
290 .await?;
291 }
292
293 Ok(account.into())
294 }
295
296 async fn account_set_create(
297 &self,
298 ctx: &Context<'_>,
299 input: AccountSetCreateInput,
300 ) -> Result<AccountSetCreatePayload> {
301 let app = ctx.data_unchecked::<CalaApp>();
302 let mut op = ctx
303 .data_unchecked::<DbOp>()
304 .try_lock()
305 .expect("Lock held concurrently");
306 let mut builder = cala_ledger::account_set::NewAccountSet::builder();
307 builder
308 .id(input.account_set_id)
309 .journal_id(input.journal_id)
310 .name(input.name)
311 .normal_balance_type(input.normal_balance_type);
312
313 if let Some(description) = input.description {
314 builder.description(description);
315 }
316 if let Some(metadata) = input.metadata {
317 builder.metadata(metadata)?;
318 }
319 let account_set = app
320 .ledger()
321 .account_sets()
322 .create_in_op(&mut *op, builder.build()?)
323 .await?;
324
325 Ok(account_set.into())
326 }
327
328 async fn account_set_update(
329 &self,
330 ctx: &Context<'_>,
331 id: UUID,
332 input: AccountSetUpdateInput,
333 ) -> Result<AccountSetUpdatePayload> {
334 let app = ctx.data_unchecked::<CalaApp>();
335 let mut op = ctx
336 .data_unchecked::<DbOp>()
337 .try_lock()
338 .expect("Lock held concurrently");
339 let mut builder = cala_ledger::account_set::AccountSetUpdate::default();
340 if let Some(name) = input.name {
341 builder.name(name);
342 }
343 if let Some(desc) = input.description {
344 builder.description(desc);
345 }
346 if let Some(normal_balance_type) = input.normal_balance_type {
347 builder.normal_balance_type(normal_balance_type);
348 }
349
350 if let Some(metadata) = input.metadata {
351 builder.metadata(metadata)?;
352 }
353
354 let mut account_set = app
355 .ledger()
356 .account_sets()
357 .find(AccountSetId::from(id))
358 .await?;
359 if account_set.update(builder).did_execute() {
360 app.ledger()
361 .account_sets()
362 .persist_in_op(&mut *op, &mut account_set)
363 .await?;
364 }
365
366 Ok(account_set.into())
367 }
368
369 async fn add_to_account_set(
370 &self,
371 ctx: &Context<'_>,
372 input: AddToAccountSetInput,
373 ) -> Result<AddToAccountSetPayload> {
374 let app = ctx.data_unchecked::<CalaApp>();
375 let mut op = ctx
376 .data_unchecked::<DbOp>()
377 .try_lock()
378 .expect("Lock held concurrently");
379
380 let account_set = app
381 .ledger()
382 .account_sets()
383 .add_member_in_op(&mut *op, AccountSetId::from(input.account_set_id), input)
384 .await?;
385
386 Ok(account_set.into())
387 }
388
389 async fn remove_from_account_set(
390 &self,
391 ctx: &Context<'_>,
392 input: RemoveFromAccountSetInput,
393 ) -> Result<RemoveFromAccountSetPayload> {
394 let app = ctx.data_unchecked::<CalaApp>();
395 let mut op = ctx
396 .data_unchecked::<DbOp>()
397 .try_lock()
398 .expect("Lock held concurrently");
399
400 let account_set = app
401 .ledger()
402 .account_sets()
403 .remove_member_in_op(&mut *op, AccountSetId::from(input.account_set_id), input)
404 .await?;
405
406 Ok(account_set.into())
407 }
408
409 async fn journal_create(
410 &self,
411 ctx: &Context<'_>,
412 input: JournalCreateInput,
413 ) -> Result<JournalCreatePayload> {
414 let app = ctx.data_unchecked::<CalaApp>();
415 let mut op = ctx
416 .data_unchecked::<DbOp>()
417 .try_lock()
418 .expect("Lock held concurrently");
419 let mut builder = cala_ledger::journal::NewJournal::builder();
420 builder
421 .id(input.journal_id)
422 .name(input.name)
423 .status(input.status);
424 if let Some(description) = input.description {
425 builder.description(description);
426 }
427 let journal = app
428 .ledger()
429 .journals()
430 .create_in_op(&mut *op, builder.build()?)
431 .await?;
432
433 Ok(journal.into())
434 }
435
436 async fn journal_update(
437 &self,
438 ctx: &Context<'_>,
439 id: UUID,
440 input: JournalUpdateInput,
441 ) -> Result<JournalUpdatePayload> {
442 let app = ctx.data_unchecked::<CalaApp>();
443 let mut op = ctx
444 .data_unchecked::<DbOp>()
445 .try_lock()
446 .expect("Lock held concurrently");
447
448 let mut builder = cala_ledger::journal::JournalUpdate::default();
449 if let Some(name) = input.name {
450 builder.name(name);
451 }
452 if let Some(status) = input.status {
453 builder.status(status);
454 }
455 if let Some(description) = input.description {
456 builder.description(description);
457 }
458
459 let mut journal = app.ledger().journals().find(JournalId::from(id)).await?;
460 if journal.update(builder).did_execute() {
461 app.ledger()
462 .journals()
463 .persist_in_op(&mut *op, &mut journal)
464 .await?;
465 }
466
467 Ok(journal.into())
468 }
469
470 async fn tx_template_create(
471 &self,
472 ctx: &Context<'_>,
473 input: TxTemplateCreateInput,
474 ) -> Result<TxTemplateCreatePayload> {
475 let app = ctx.data_unchecked::<CalaApp>();
476 let mut op = ctx
477 .data_unchecked::<DbOp>()
478 .try_lock()
479 .expect("Lock held concurrently");
480 let mut new_tx_template_transaction_builder =
481 cala_ledger::tx_template::NewTxTemplateTransaction::builder();
482 let TxTemplateTransactionInput {
483 effective,
484 journal_id,
485 correlation_id,
486 external_id,
487 description,
488 metadata,
489 } = input.transaction;
490 new_tx_template_transaction_builder
491 .effective(effective)
492 .journal_id(journal_id);
493 if let Some(correlation_id) = correlation_id {
494 new_tx_template_transaction_builder.correlation_id(correlation_id);
495 };
496 if let Some(external_id) = external_id {
497 new_tx_template_transaction_builder.external_id(external_id);
498 };
499 if let Some(description) = description {
500 new_tx_template_transaction_builder.description(description);
501 };
502 if let Some(metadata) = metadata {
503 new_tx_template_transaction_builder.metadata(metadata);
504 }
505 let new_transaction = new_tx_template_transaction_builder.build()?;
506
507 let mut new_params = Vec::new();
508 if let Some(params) = input.params {
509 for param in params {
510 let mut param_builder = NewParamDefinition::builder();
511 param_builder.name(param.name).r#type(param.r#type.into());
512 if let Some(default) = param.default {
513 param_builder.default_expr(default);
514 }
515 if let Some(desc) = param.description {
516 param_builder.description(desc);
517 }
518 let new_param = param_builder.build()?;
519 new_params.push(new_param);
520 }
521 }
522
523 let mut new_entries = Vec::new();
524 for entry in input.entries {
525 let TxTemplateEntryInput {
526 entry_type,
527 account_id,
528 layer,
529 direction,
530 units,
531 currency,
532 description,
533 } = entry;
534 let mut new_entry_input_builder =
535 cala_ledger::tx_template::NewTxTemplateEntry::builder();
536 new_entry_input_builder
537 .entry_type(entry_type)
538 .account_id(account_id)
539 .layer(layer)
540 .direction(direction)
541 .units(units)
542 .currency(currency);
543 if let Some(desc) = description {
544 new_entry_input_builder.description(desc);
545 }
546 let new_entry_input = new_entry_input_builder.build()?;
547 new_entries.push(new_entry_input);
548 }
549
550 let mut new_tx_template_builder = cala_ledger::tx_template::NewTxTemplate::builder();
551 new_tx_template_builder
552 .id(input.tx_template_id)
553 .code(input.code)
554 .transaction(new_transaction)
555 .params(new_params)
556 .entries(new_entries);
557 if let Some(desc) = input.description {
558 new_tx_template_builder.description(desc);
559 }
560 if let Some(metadata) = input.metadata {
561 new_tx_template_builder.metadata(metadata)?;
562 }
563 let new_tx_template = new_tx_template_builder.build()?;
564
565 let tx_template = app
566 .ledger()
567 .tx_templates()
568 .create_in_op(&mut *op, new_tx_template)
569 .await?;
570
571 Ok(tx_template.into())
572 }
573
574 async fn transaction_post(
575 &self,
576 ctx: &Context<'_>,
577 input: TransactionInput,
578 ) -> Result<TransactionPostPayload> {
579 let app = ctx.data_unchecked::<CalaApp>();
580 let mut op = ctx
581 .data_unchecked::<DbOp>()
582 .try_lock()
583 .expect("Lock held concurrently");
584 let params = input.params.map(cala_ledger::tx_template::Params::from);
585 let transaction = app
586 .ledger()
587 .post_transaction_in_op(
588 &mut *op,
589 input.transaction_id.into(),
590 &input.tx_template_code,
591 params.unwrap_or_default(),
592 )
593 .await?;
594 Ok(transaction.into())
595 }
596
597 async fn velocity_limit_create(
598 &self,
599 ctx: &Context<'_>,
600 input: VelocityLimitCreateInput,
601 ) -> Result<VelocityLimitCreatePayload> {
602 let app = ctx.data_unchecked::<CalaApp>();
603 let mut op = ctx
604 .data_unchecked::<DbOp>()
605 .try_lock()
606 .expect("Lock held concurrently");
607
608 let mut new_velocity_limit_builder = cala_ledger::velocity::NewVelocityLimit::builder();
609 new_velocity_limit_builder
610 .id(input.velocity_limit_id)
611 .name(input.name)
612 .description(input.description);
613
614 if let Some(condition) = input.condition {
615 new_velocity_limit_builder.condition(condition);
616 }
617
618 if let Some(currency) = input.currency {
619 new_velocity_limit_builder.currency(currency);
620 }
621
622 let mut new_window = Vec::new();
623 for partition_key_input in input.window {
624 let mut new_partition_key_builder = cala_ledger::velocity::NewPartitionKey::builder();
625 let partition_key = new_partition_key_builder
626 .alias(partition_key_input.alias)
627 .value(partition_key_input.value)
628 .build()?;
629
630 new_window.push(partition_key);
631 }
632 new_velocity_limit_builder.window(new_window);
633
634 let mut new_limit_builder = cala_ledger::velocity::NewLimit::builder();
635
636 if let Some(timestamp_source) = input.limit.timestamp_source {
637 new_limit_builder.timestamp_source(timestamp_source);
638 }
639
640 let mut new_balance_limits = Vec::new();
641 for balance_limit_input in input.limit.balance {
642 let mut new_balance_limit_builder = cala_ledger::velocity::NewBalanceLimit::builder();
643 new_balance_limit_builder
644 .limit_type(balance_limit_input.limit_type)
645 .layer(balance_limit_input.layer)
646 .amount(balance_limit_input.amount)
647 .enforcement_direction(balance_limit_input.normal_balance_type);
648
649 if let Some(start) = balance_limit_input.start {
650 new_balance_limit_builder.start(start);
651 }
652
653 if let Some(end) = balance_limit_input.end {
654 new_balance_limit_builder.end(end);
655 }
656
657 let new_balance_limit = new_balance_limit_builder.build()?;
658 new_balance_limits.push(new_balance_limit);
659 }
660 new_limit_builder.balance(new_balance_limits);
661 let new_limit = new_limit_builder.build()?;
662
663 new_velocity_limit_builder.limit(new_limit);
664
665 if let Some(params) = input.params {
666 let mut new_params = Vec::new();
667 for param in params {
668 let mut param_builder = NewParamDefinition::builder();
669 param_builder.name(param.name).r#type(param.r#type.into());
670 if let Some(default) = param.default {
671 param_builder.default_expr(default);
672 }
673 if let Some(description) = param.description {
674 param_builder.description(description);
675 }
676 let new_param = param_builder.build()?;
677 new_params.push(new_param);
678 }
679 new_velocity_limit_builder.params(new_params);
680 }
681
682 let new_velocity_limit = new_velocity_limit_builder.build()?;
683
684 let velocity_limit = app
685 .ledger()
686 .velocities()
687 .create_limit_in_op(&mut *op, new_velocity_limit)
688 .await?;
689
690 Ok(velocity_limit.into())
691 }
692
693 async fn velocity_control_create(
694 &self,
695 ctx: &Context<'_>,
696 input: VelocityControlCreateInput,
697 ) -> Result<VelocityControlCreatePayload> {
698 let app = ctx.data_unchecked::<CalaApp>();
699 let mut op = ctx
700 .data_unchecked::<DbOp>()
701 .try_lock()
702 .expect("Lock held concurrently");
703
704 let mut new_velocity_control_builder = cala_ledger::velocity::NewVelocityControl::builder();
705 new_velocity_control_builder
706 .id(input.velocity_control_id)
707 .name(input.name)
708 .description(input.description);
709
710 if let Some(condition) = input.condition {
711 new_velocity_control_builder.condition(condition);
712 }
713
714 let mut new_velocity_enforcement_builder =
715 cala_ledger::velocity::NewVelocityEnforcement::builder();
716 new_velocity_enforcement_builder.action(input.enforcement.velocity_enforcement_action);
717 let new_velocity_enforcement = new_velocity_enforcement_builder.build()?;
718
719 new_velocity_control_builder.enforcement(new_velocity_enforcement);
720
721 let new_velocity_control = new_velocity_control_builder.build()?;
722 let velocity_control = app
723 .ledger()
724 .velocities()
725 .create_control_in_op(&mut *op, new_velocity_control)
726 .await?;
727
728 Ok(velocity_control.into())
729 }
730
731 async fn velocity_control_add_limit(
732 &self,
733 ctx: &Context<'_>,
734 input: VelocityControlAddLimitInput,
735 ) -> Result<VelocityControlAddLimitPayload> {
736 let app = ctx.data_unchecked::<CalaApp>();
737 let mut op = ctx
738 .data_unchecked::<DbOp>()
739 .try_lock()
740 .expect("Lock held concurrently");
741
742 let velocity_limit = app
743 .ledger()
744 .velocities()
745 .add_limit_to_control_in_op(
746 &mut *op,
747 input.velocity_control_id.into(),
748 input.velocity_limit_id.into(),
749 )
750 .await?;
751
752 Ok(velocity_limit.into())
753 }
754
755 async fn velocity_control_attach(
756 &self,
757 ctx: &Context<'_>,
758 input: VelocityControlAttachInput,
759 ) -> Result<VelocityControlAttachPayload> {
760 let app = ctx.data_unchecked::<CalaApp>();
761 let mut op = ctx
762 .data_unchecked::<DbOp>()
763 .try_lock()
764 .expect("Lock held concurrently");
765 let params = cala_ledger::tx_template::Params::from(input.params);
766
767 let velocity_control = app
768 .ledger()
769 .velocities()
770 .attach_control_to_account_in_op(
771 &mut *op,
772 input.velocity_control_id.into(),
773 input.account_id.into(),
774 params,
775 )
776 .await?;
777
778 Ok(velocity_control.into())
779 }
780}