Repository: TBD54566975/ssi-service
Pull request #201 (2022-12-08 17:28:26)

[OSE-177] Added GetOperation and ReviewSubmission impl
# Overview
This includes the implementation of the ReviewSubmission, and the GetOperation endpoints. 

# Description
Most of the meat and bones of this is in the `pkg/storage/bolt.go` file, where I made changes to operations and submissions are updated in a single transaction. Noteworthy items:
* Key interfaces that were introduced in order to ensure that storage layer was decoupled from the service layer.
* Moved the `pkg/service/presentation/model.go` into it's own package, to avoid import cycles.

# How Has This Been Tested?
See all test files modified. A key aspect is that once the operation is done, the `GetOperation` will return something like below:
```json
{
  "id": "presentations/submissions/1237489",
  "done": true,
  "result": {
    "response": {
      "status":"approved",
      "reason":"fancy operator said it was ok",
      "id": ...
      "definition_id":...
      "definition_map":...
    }
  }
}
```

# Checklist

Before submitting this PR, please make sure:

- [x] I have read the CONTRIBUTING document.
- [x] My code is consistent with the rest of the project 
- [x] I have tagged the relevant reviewers and/or interested parties
- [x] I have updated the READMEs and other documentation of affected packages

## References
SIP6


codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/201?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#201](https://codecov.io/gh/TBD54566975/ssi-service/pull/201?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (98e6b3a) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/adbbcaea0da5bf5e553dc949a9ae4c8e03d79e98?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (adbbcae) will **increase** coverage by `22.67%`.
> The diff coverage is `27.67%`.

```diff
@@            Coverage Diff            @@
##           main     #201       +/-   ##
=========================================
+ Coverage      0   22.67%   +22.67%     
=========================================
  Files         0       26       +26     
  Lines         0     2284     +2284     
=========================================
+ Hits          0      518      +518     
- Misses        0     1684     +1684     
- Partials      0       82       +82     
```


| [Impacted Files](https://codecov.io/gh/TBD54566975/ssi-service/pull/201?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/router/operation.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvb3BlcmF0aW9uLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/server/router/presentation.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvcHJlc2VudGF0aW9uLmdv) | `3.65% <0.00%> (ø)` | |
| [pkg/service/operation/model.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uvb3BlcmF0aW9uL21vZGVsLmdv) | `50.00% <0.00%> (ø)` | |
| [pkg/service/operation/service.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uvb3BlcmF0aW9uL3NlcnZpY2UuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/storage/bolt.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3N0b3JhZ2UvYm9sdC5nbw==) | `70.58% <70.96%> (ø)` | |
| [pkg/server/router/keystore.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIva2V5c3RvcmUuZ28=) | `12.69% <0.00%> (ø)` | |
| [pkg/server/server.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9zZXJ2ZXIuZ28=) | `81.59% <0.00%> (ø)` | |
| [integration/common.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-aW50ZWdyYXRpb24vY29tbW9uLmdv) | `0.00% <0.00%> (ø)` | |
| [integration/testutil.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-aW50ZWdyYXRpb24vdGVzdHV0aWwuZ28=) | `29.41% <0.00%> (ø)` | |
| ... and [21 more](https://codecov.io/gh/TBD54566975/ssi-service/pull/201/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)


Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index 0052fcb..5e84b49 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -766,8 +766,11 @@ definitions:
         type: array
       id:
         type: string
+      reason:
+        description: The reason why the submission was approved or denied.
+        type: string
       status:
-        description: One of {`pending`, `done`}.
+        description: One of {`pending`, `approved`, `denied`}.
         type: string
     required:
     - definition_id
@@ -831,8 +834,25 @@ definitions:
     type: object
   github.com_tbd54566975_ssi-service_pkg_server_router.ReviewSubmissionResponse:
     properties:
-      submission:
-        $ref: '#/definitions/exchange.PresentationSubmission'
+      definition_id:
+        type: string
+      descriptor_map:
+        items:
+          $ref: '#/definitions/exchange.SubmissionDescriptor'
+        type: array
+      id:
+        type: string
+      reason:
+        description: The reason why the submission was approved or denied.
+        type: string
+      status:
+        description: One of {`pending`, `approved`, `denied`}.
+        type: string
+    required:
+    - definition_id
+    - descriptor_map
+    - id
+    - status
     type: object
   github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest:
     properties:
@@ -1318,8 +1338,11 @@ definitions:
         type: array
       id:
         type: string
+      reason:
+        description: The reason why the submission was approved or denied.
+        type: string
       status:
-        description: One of {`pending`, `done`}.
+        description: One of {`pending`, `approved`, `denied`}.
         type: string
     required:
     - definition_id
@@ -1383,8 +1406,25 @@ definitions:
     type: object
   pkg_server_router.ReviewSubmissionResponse:
     properties:
-      submission:
-        $ref: '#/definitions/exchange.PresentationSubmission'
+      definition_id:
+        type: string
+      descriptor_map:
+        items:
+          $ref: '#/definitions/exchange.SubmissionDescriptor'
+        type: array
+      id:
+        type: string
+      reason:
+        description: The reason why the submission was approved or denied.
+        type: string
+      status:
+        description: One of {`pending`, `approved`, `denied`}.
+        type: string
+    required:
+    - definition_id
+    - descriptor_map
+    - id
+    - status
     type: object
   pkg_server_router.StoreKeyRequest:
     properties:
@@ -1471,8 +1511,11 @@ definitions:
         type: array
       id:
         type: string
+      reason:
+        description: The reason why the submission was approved or denied.
+        type: string
       status:
-        description: One of {`pending`, `done`}.
+        description: One of {`pending`, `approved`, `denied`}.
         type: string
     required:
     - definition_id
@@ -1871,7 +1914,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetDIDMethodsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDMethodsResponse'
       summary: Get DID Methods
       tags:
       - DecentralizedIdentityAPI
@@ -1892,7 +1935,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetDIDsByMethodResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDsByMethodResponse'
         "400":
           description: Bad request
           schema:
@@ -1910,7 +1953,7 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodRequest'
       - description: Method
         in: path
         name: method
@@ -1922,7 +1965,7 @@ paths:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreateDIDByMethodResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodResponse'
         "400":
           description: Bad request
           schema:
@@ -1945,7 +1988,7 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodRequest'
       - description: Method
         in: path
         name: method
@@ -1962,7 +2005,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetDIDByMethodResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDByMethodResponse'
         "400":
           description: Bad request
           schema:
@@ -1987,7 +2030,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.ResolveDIDResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.ResolveDIDResponse'
         "400":
           description: Bad request
           schema:
@@ -2492,52 +2535,55 @@ paths:
       summary: Get PresentationDefinition
       tags:
       - PresentationDefinitionAPI
-  /v1/presentations/submission/{id}:
+  /v1/presentations/submissions:
     get:
       consumes:
       - application/json
-      description: Get a submission by its ID
+      description: List existing submissions according to a filtering query. The `filter`
+        field follows the syntax described in https://google.aip.dev/160.
       parameters:
-      - description: ID
-        in: path
-        name: id
+      - description: request body
+        in: body
+        name: request
         required: true
-        type: string
+        schema:
+          $ref: '#/definitions/pkg_server_router.ListSubmissionRequest'
       produces:
       - application/json
       responses:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetSubmissionResponse'
+            $ref: '#/definitions/pkg_server_router.ListSubmissionResponse'
         "400":
           description: Bad request
           schema:
             type: string
-      summary: Get Submission
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: List Submissions
       tags:
       - SubmissionAPI
-  /v1/presentations/submissions:
-    get:
+    put:
       consumes:
       - application/json
-      description: Reviews a pending submission. After this method is called, the
-        operation with `id==presentations/submissions/{submission_id}` will be updated
-        with the result of this invocation.
+      description: Creates a submission in this server ready to be reviewed.
       parameters:
       - description: request body
         in: body
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.ReviewSubmissionRequest'
+          $ref: '#/definitions/pkg_server_router.CreateSubmissionRequest'
       produces:
       - application/json
       responses:
-        "200":
-          description: OK
+        "201":
+          description: The type of response is Submission once the operation has finished.
           schema:
-            $ref: '#/definitions/pkg_server_router.ReviewSubmissionResponse'
+            $ref: '#/definitions/pkg_server_router.Operation'
         "400":
           description: Bad request
           schema:
@@ -2546,27 +2592,55 @@ paths:
           description: Internal server error
           schema:
             type: string
-      summary: Review a pending submissions
+      summary: Create Submission
+      tags:
+      - SubmissionAPI
+  /v1/presentations/submissions/{id}:
+    get:
+      consumes:
+      - application/json
+      description: Get a submission by its ID
+      parameters:
+      - description: ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/pkg_server_router.GetSubmissionResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+      summary: Get Submission
       tags:
       - SubmissionAPI
+  /v1/presentations/submissions/{id}/review:
     put:
       consumes:
       - application/json
-      description: Creates a submission in this server ready to be reviewed.
+      description: Reviews a pending submission. After this method is called, the
+        operation with `id==presentations/submissions/{submission_id}` will be updated
+        with the result of this invocation.
       parameters:
       - description: request body
         in: body
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateSubmissionRequest'
+          $ref: '#/definitions/pkg_server_router.ReviewSubmissionRequest'
       produces:
       - application/json
       responses:
-        "201":
-          description: The type of response is Submission once the operation has finished.
+        "200":
+          description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.Operation'
+            $ref: '#/definitions/pkg_server_router.ReviewSubmissionResponse'
         "400":
           description: Bad request
           schema:
@@ -2575,7 +2649,7 @@ paths:
           description: Internal server error
           schema:
             type: string
-      summary: Create Submission
+      summary: Review a pending submissions
       tags:
       - SubmissionAPI
   /v1/schemas:
diff --git a/pkg/server/router/operation.go b/pkg/server/router/operation.go
index 20c6535..f4d8410 100644
--- a/pkg/server/router/operation.go
+++ b/pkg/server/router/operation.go
@@ -39,8 +39,20 @@ func NewOperationRouter(s svcframework.Service) (*OperationRouter, error) {
 // @Failure      400  {string}  string  "Bad request"
 // @Failure      500  {string}  string  "Internal server error"
 // @Router       /v1/operations/{id} [get]
-func (pdr OperationRouter) GetOperation(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-	return nil
+func (o OperationRouter) GetOperation(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+	id := framework.GetParam(ctx, IDParam)
+	if id == nil {
+		return framework.NewRequestError(
+			util.LoggingNewError("get operation request requires id"), http.StatusBadRequest)
+	}
+
+	op, err := o.service.GetOperation(operation.GetOperationRequest{ID: *id})
+
+	if err != nil {
+		return framework.NewRequestError(
+			util.LoggingErrorMsg(err, "failed getting operation"), http.StatusInternalServerError)
+	}
+	return framework.Respond(ctx, w, routerModel(op), http.StatusOK)
 }
 
 type GetOperationsRequest struct {
@@ -108,7 +120,7 @@ type GetOperationsResponse struct {
 // @Failure      400  {string}  string  "Bad request"
 // @Failure      500  {string}  string  "Internal server error"
 // @Router       /v1/operations [get]
-func (pdr OperationRouter) GetOperations(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+func (o OperationRouter) GetOperations(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 	var request GetOperationsRequest
 	if err := framework.Decode(r, &request); err != nil {
 		return framework.NewRequestError(
@@ -126,21 +138,29 @@ func (pdr OperationRouter) GetOperations(ctx context.Context, w http.ResponseWri
 			util.LoggingErrorMsg(err, "invalid get operations request"), http.StatusBadRequest)
 	}
 
-	ops, err := pdr.service.GetOperations(req)
+	ops, err := o.service.GetOperations(req)
 	if err != nil {
 		logrus.WithError(err).Error("getting operations from service")
 		return framework.NewRequestError(err, http.StatusInternalServerError)
 	}
-	resp := GetOperationsResponse{Operations: make([]Operation, len(ops.Operations))}
-	for i, op := range ops.Operations {
-		resp.Operations[i].ID = op.ID
-		resp.Operations[i].Done = op.Done
-		resp.Operations[i].Result.Error = op.Result.Error
-		resp.Operations[i].Result.Response = op.Result.Response
+	resp := GetOperationsResponse{Operations: make([]Operation, 0, len(ops.Operations))}
+	for _, op := range ops.Operations {
+		resp.Operations = append(resp.Operations, routerModel(op))
 	}
 	return framework.Respond(ctx, w, resp, http.StatusOK)
 }
 
+func routerModel(op operation.Operation) Operation {
+	return Operation{
+		ID:   op.ID,
+		Done: op.Done,
+		Result: OperationResult{
+			Error:    op.Result.Error,
+			Response: op.Result.Response,
+		},
+	}
+}
+
 // CancelOperation godoc
 // @Summary      Cancel an ongoing operation
 // @Description  Cancels an ongoing operation, if possible.
@@ -152,6 +172,6 @@ func (pdr OperationRouter) GetOperations(ctx context.Context, w http.ResponseWri
 // @Failure      400  {string}  string  "Bad request"
 // @Failure      500  {string}  string  "Internal server error"
 // @Router       /v1/operations [get]
-func (pdr OperationRouter) CancelOperation(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+func (o OperationRouter) CancelOperation(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 	return nil
 }
diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go
index c41c050..bc1d2e7 100644
--- a/pkg/server/router/presentation.go
+++ b/pkg/server/router/presentation.go
@@ -14,6 +14,7 @@ import (
 	"github.com/tbd54566975/ssi-service/pkg/server/framework"
 	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation/model"
 	"go.einride.tech/aip/filtering"
 	"net/http"
 )
@@ -74,7 +75,7 @@ func (pr PresentationRouter) CreatePresentationDefinition(ctx context.Context, w
 		logrus.WithError(err).Error(errMsg)
 		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
 	}
-	serviceResp, err := pr.service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *def})
+	serviceResp, err := pr.service.CreatePresentationDefinition(model.CreatePresentationDefinitionRequest{PresentationDefinition: *def})
 	if err != nil {
 		logrus.WithError(err).Error(errMsg)
 		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError)
@@ -139,7 +140,7 @@ func (pr PresentationRouter) GetPresentationDefinition(ctx context.Context, w ht
 		return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
 	}
 
-	def, err := pr.service.GetPresentationDefinition(presentation.GetPresentationDefinitionRequest{ID: *id})
+	def, err := pr.service.GetPresentationDefinition(model.GetPresentationDefinitionRequest{ID: *id})
 	if err != nil {
 		errMsg := fmt.Sprintf("could not get presentation with id: %s", *id)
 		logrus.WithError(err).Error(errMsg)
@@ -171,7 +172,7 @@ func (pr PresentationRouter) DeletePresentationDefinition(ctx context.Context, w
 		return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
 	}
 
-	if err := pr.service.DeletePresentationDefinition(presentation.DeletePresentationDefinitionRequest{ID: *id}); err != nil {
+	if err := pr.service.DeletePresentationDefinition(model.DeletePresentationDefinitionRequest{ID: *id}); err != nil {
 		errMsg := fmt.Sprintf("could not delete presentation with id: %s", *id)
 		logrus.WithError(err).Error(errMsg)
 		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError)
@@ -184,7 +185,7 @@ type CreateSubmissionRequest struct {
 	SubmissionJWT keyaccess.JWT `json:"submissionJwt" validate:"required"`
 }
 
-func (r CreateSubmissionRequest) toServiceRequest() (*presentation.CreateSubmissionRequest, error) {
+func (r CreateSubmissionRequest) toServiceRequest() (*model.CreateSubmissionRequest, error) {
 	sdkVP, err := signing.ParseVerifiablePresentationFromJWT(r.SubmissionJWT.String())
 	if err != nil {
 		return nil, errors.Wrap(err, "parsing presentation from jwt")
@@ -211,7 +212,7 @@ func (r CreateSubmissionRequest) toServiceRequest() (*presentation.CreateSubmiss
 		return nil, errors.Wrap(err, "parsing verifiable credential array")
 	}
 
-	return &presentation.CreateSubmissionRequest{
+	return &model.CreateSubmissionRequest{
 		Presentation:  *sdkVP,
 		SubmissionJWT: r.SubmissionJWT,
 		Submission:    s,
@@ -274,7 +275,7 @@ func (pr PresentationRouter) CreateSubmission(ctx context.Context, w http.Respon
 }
 
 type GetSubmissionResponse struct {
-	*presentation.Submission
+	*model.Submission
 }
 
 // GetSubmission godoc
@@ -286,7 +287,7 @@ type GetSubmissionResponse struct {
 // @Param        id   path      string  true  "ID"
 // @Success      200  {object}  GetSubmissionResponse
 // @Failure      400  {string}  string  "Bad request"
-// @Router       /v1/presentations/submission/{id} [get]
+// @Router       /v1/presentations/submissions/{id} [get]
 func (pr PresentationRouter) GetSubmission(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 	id := framework.GetParam(ctx, IDParam)
 	if id == nil {
@@ -294,7 +295,7 @@ func (pr PresentationRouter) GetSubmission(ctx context.Context, w http.ResponseW
 			util.LoggingNewError("get submission request requires id"), http.StatusBadRequest)
 	}
 
-	submission, err := pr.service.GetSubmission(presentation.GetSubmissionRequest{ID: *id})
+	submission, err := pr.service.GetSubmission(model.GetSubmissionRequest{ID: *id})
 
 	if err != nil {
 		return framework.NewRequestError(
@@ -317,7 +318,7 @@ func (l ListSubmissionRequest) GetFilter() string {
 }
 
 type ListSubmissionResponse struct {
-	Submissions []presentation.Submission `json:"submissions"`
+	Submissions []model.Submission `json:"submissions"`
 }
 
 // ListSubmissions godoc
@@ -363,7 +364,7 @@ func (pr PresentationRouter) ListSubmissions(ctx context.Context, w http.Respons
 		return framework.NewRequestError(
 			util.LoggingErrorMsg(err, "invalid filter"), http.StatusBadRequest)
 	}
-	resp, err := pr.service.ListSubmissions(presentation.ListSubmissionRequest{
+	resp, err := pr.service.ListSubmissions(model.ListSubmissionRequest{
 		Filter: filter,
 	})
 	if err != nil {
@@ -378,8 +379,16 @@ type ReviewSubmissionRequest struct {
 	Reason   string `json:"reason"`
 }
 
+func (r ReviewSubmissionRequest) toServiceRequest(id string) model.ReviewSubmissionRequest {
+	return model.ReviewSubmissionRequest{
+		ID:       id,
+		Approved: r.Approved,
+		Reason:   r.Reason,
+	}
+}
+
 type ReviewSubmissionResponse struct {
-	Submission exchange.PresentationSubmission `json:"submission"`
+	*model.Submission
 }
 
 // ReviewSubmission godoc
@@ -392,7 +401,27 @@ type ReviewSubmissionResponse struct {
 // @Success      200  {object}  ReviewSubmissionResponse
 // @Failure      400  {string}  string  "Bad request"
 // @Failure      500  {string}  string  "Internal server error"
-// @Router       /v1/presentations/submissions [get]
+// @Router       /v1/presentations/submissions/{id}/review [put]
 func (pr PresentationRouter) ReviewSubmission(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-	return framework.Respond(ctx, w, ReviewSubmissionResponse{}, http.StatusOK)
+	id := framework.GetParam(ctx, IDParam)
+	if id == nil {
+		return framework.NewRequestError(
+			util.LoggingNewError("review submission request requires id"), http.StatusBadRequest)
+	}
+
+	var request ReviewSubmissionRequest
+	if err := framework.Decode(r, &request); err != nil {
+		return framework.NewRequestError(
+			util.LoggingErrorMsg(err, "invalid review submissions request"), http.StatusBadRequest)
+	}
+
+	req := request.toServiceRequest(*id)
+	submission, err := pr.service.ReviewSubmission(req)
+	if err != nil {
+		return framework.NewRequestError(
+			util.LoggingErrorMsg(err, "failed reviewing submission"), http.StatusInternalServerError)
+	}
+	return framework.Respond(ctx, w, ReviewSubmissionResponse{
+		Submission: submission,
+	}, http.StatusOK)
 }
diff --git a/pkg/service/operation/model.go b/pkg/service/operation/model.go
index 959734c..7131084 100644
--- a/pkg/service/operation/model.go
+++ b/pkg/service/operation/model.go
@@ -1,14 +1,16 @@
 package operation
 
 import (
+	"fmt"
 	"github.com/TBD54566975/ssi-sdk/util"
+	"github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
 	"go.einride.tech/aip/filtering"
 	"strings"
 )
 
 type Result struct {
-	Error    string      `json:"error,omitempty"`
-	Response interface{} `json:"response,omitempty"`
+	Error    string `json:"error,omitempty"`
+	Response any    `json:"response,omitempty"`
 }
 
 type Operation struct {
@@ -39,3 +41,17 @@ func (r GetOperationsRequest) Validate() error {
 type GetOperationsResponse struct {
 	Operations []Operation
 }
+
+type GetOperationRequest struct {
+	ID string `json:"id" validate:"required"`
+}
+
+// Validate does struct validation and returns an error when invalid.
+func (r GetOperationRequest) Validate() error {
+	return util.NewValidator().Struct(r)
+}
+
+// IDFromSubmissionID returns a submission operation ID from the submission ID.
+func IDFromSubmissionID(id string) string {
+	return fmt.Sprintf("%s/%s", storage.SubmissionParentResource, id)
+}
diff --git a/pkg/service/operation/service.go b/pkg/service/operation/service.go
index 01cef07..7ca01c9 100644
--- a/pkg/service/operation/service.go
+++ b/pkg/service/operation/service.go
@@ -3,11 +3,16 @@ package operation
 import (
 	"fmt"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
+	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation/model"
+	prestorage "github.com/tbd54566975/ssi-service/pkg/service/presentation/storage"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
+	"strings"
 )
 
 type Service struct {
@@ -47,19 +52,55 @@ func (s Service) GetOperations(request GetOperationsRequest) (*GetOperationsResp
 	}
 	for i, op := range ops {
 		op := op
-		newOp := Operation{
-			ID:   op.ID,
-			Done: op.Done,
-			Result: Result{
-				Error:    op.Error,
-				Response: op.Response,
-			},
+		newOp, err := serviceModel(op)
+		if err != nil {
+			logrus.WithError(err).WithField("operation_id", op.ID).Error("converting to storage operations to model")
+			continue
 		}
 		resp.Operations[i] = newOp
 	}
 	return resp, nil
 }
 
+type ServiceModelFunc func(any) any
+
+func serviceModel(op opstorage.StoredOperation) (Operation, error) {
+	newOp := Operation{
+		ID:   op.ID,
+		Done: op.Done,
+		Result: Result{
+			Error: op.Error,
+		},
+	}
+
+	if len(op.Response) > 0 {
+		switch {
+		case strings.HasPrefix(op.ID, opstorage.SubmissionParentResource):
+			var s prestorage.StoredSubmission
+			if err := json.Unmarshal(op.Response, &s); err != nil {
+				return Operation{}, err
+			}
+			newOp.Result.Response = model.ServiceModel(&s)
+		default:
+			return newOp, errors.New("unknown response type")
+		}
+	}
+
+	return newOp, nil
+}
+
+func (s Service) GetOperation(request GetOperationRequest) (Operation, error) {
+	if err := request.Validate(); err != nil {
+		return Operation{}, errors.Wrap(err, "invalid request")
+	}
+
+	storedOp, err := s.storage.GetOperation(request.ID)
+	if err != nil {
+		return Operation{}, errors.Wrap(err, "fetching from storage")
+	}
+	return serviceModel(storedOp)
+}
+
 func NewOperationService(s storage.ServiceStorage) (*Service, error) {
 	opStorage, err := opstorage.NewOperationStorage(s)
 	if err != nil {
diff --git a/pkg/service/operation/storage/bolt.go b/pkg/service/operation/storage/bolt.go
index d28eb5e..e52cb14 100644
--- a/pkg/service/operation/storage/bolt.go
+++ b/pkg/service/operation/storage/bolt.go
@@ -1,7 +1,6 @@
 package storage
 
 import (
-	"fmt"
 	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
@@ -12,13 +11,14 @@ import (
 )
 
 const (
-	namespace  = "operation"
-	submission = "submission"
+	namespace = "operation_submission"
 )
 
 const SubmissionParentResource = "/presentations/submissions"
 
-func namespaceFromID(id string) string {
+// NamespaceFromID returns a namespace from a given operation ID. An empty string is returned when the namespace cannot
+// be determined.
+func NamespaceFromID(id string) string {
 	i := strings.LastIndex(id, "/")
 	if i == -1 {
 		return ""
@@ -29,7 +29,7 @@ func namespaceFromID(id string) string {
 func namespaceFromParent(parent string) string {
 	switch parent {
 	case SubmissionParentResource:
-		return fmt.Sprintf("%s_%s", namespace, submission)
+		return namespace
 	default:
 		return ""
 	}
@@ -48,22 +48,22 @@ func (b BoltOperationStorage) StoreOperation(op StoredOperation) error {
 	if err != nil {
 		return util.LoggingErrorMsgf(err, "marshalling operation with id: %s", id)
 	}
-	return b.db.Write(namespaceFromID(id), id, jsonBytes)
+	return b.db.Write(NamespaceFromID(id), id, jsonBytes)
 }
 
-func (b BoltOperationStorage) GetOperation(id string) (*StoredOperation, error) {
-	jsonBytes, err := b.db.Read(namespaceFromID(id), id)
+func (b BoltOperationStorage) GetOperation(id string) (StoredOperation, error) {
+	var stored StoredOperation
+	jsonBytes, err := b.db.Read(NamespaceFromID(id), id)
 	if err != nil {
-		return nil, util.LoggingErrorMsgf(err, "reading operation with id: %s", id)
+		return stored, util.LoggingErrorMsgf(err, "reading operation with id: %s", id)
 	}
 	if len(jsonBytes) == 0 {
-		return nil, util.LoggingNewErrorf("operation not found with id: %s", id)
+		return stored, util.LoggingNewErrorf("operation not found with id: %s", id)
 	}
-	var stored StoredOperation
 	if err := json.Unmarshal(jsonBytes, &stored); err != nil {
-		return nil, util.LoggingErrorMsgf(err, "unmarshalling stored operation: %s", id)
+		return stored, util.LoggingErrorMsgf(err, "unmarshalling stored operation: %s", id)
 	}
-	return &stored, nil
+	return stored, nil
 }
 
 func (b BoltOperationStorage) GetOperations(parent string, filter filtering.Filter) ([]StoredOperation, error) {
@@ -96,7 +96,7 @@ func (b BoltOperationStorage) GetOperations(parent string, filter filtering.Filt
 }
 
 func (b BoltOperationStorage) DeleteOperation(id string) error {
-	if err := b.db.Delete(namespaceFromID(id), id); err != nil {
+	if err := b.db.Delete(NamespaceFromID(id), id); err != nil {
 		return util.LoggingErrorMsgf(err, "deleting operation: %s", id)
 	}
 	return nil
diff --git a/pkg/service/operation/storage/storage.go b/pkg/service/operation/storage/storage.go
index dae9f9b..1ac3b4c 100644
--- a/pkg/service/operation/storage/storage.go
+++ b/pkg/service/operation/storage/storage.go
@@ -16,7 +16,7 @@ type StoredOperation struct {
 	Error string `json:"errorResult,omitempty"`
 
 	// Populated only when Done == true and Error == ""
-	Response interface{} `json:"result,omitempty"`
+	Response []byte `json:"response,omitempty"`
 }
 
 func (s StoredOperation) FilterVariablesMap() map[string]interface{} {
@@ -32,7 +32,7 @@ func (s StoredOperation) FilterVariablesMap() map[string]interface{} {
 
 type Storage interface {
 	StoreOperation(op StoredOperation) error
-	GetOperation(id string) (*StoredOperation, error)
+	GetOperation(id string) (StoredOperation, error)
 	GetOperations(parent string, filter filtering.Filter) ([]StoredOperation, error)
 	DeleteOperation(id string) error
 }
diff --git a/pkg/service/presentation/model.go b/pkg/service/presentation/model/model.go
similarity index 71%
rename from pkg/service/presentation/model.go
rename to pkg/service/presentation/model/model.go
index 3ab4d6c..47cada2 100644
--- a/pkg/service/presentation/model.go
+++ b/pkg/service/presentation/model/model.go
@@ -1,4 +1,4 @@
-package presentation
+package model
 
 import (
 	credsdk "github.com/TBD54566975/ssi-sdk/credential"
@@ -6,6 +6,7 @@ import (
 	"github.com/TBD54566975/ssi-sdk/util"
 	"github.com/tbd54566975/ssi-service/internal/credential"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation/storage"
 	"go.einride.tech/aip/filtering"
 )
 
@@ -66,11 +67,33 @@ type ListSubmissionRequest struct {
 }
 
 type Submission struct {
-	// One of {`pending`, `done`}.
+	// One of {`pending`, `approved`, `denied`}.
 	Status string `json:"status" validate:"required"`
+	// The reason why the submission was approved or denied.
+	Reason string `json:"reason"`
 	*exchange.PresentationSubmission
 }
 
 type ListSubmissionResponse struct {
 	Submissions []Submission `json:"submissions"`
 }
+
+type ReviewSubmissionRequest struct {
+	ID       string `json:"id" validate:"required"`
+	Approved bool   `json:"approved"`
+	Reason   string `json:"reason"`
+}
+
+// Validate runs validation on the request struct and returns errors when it's invalid.
+func (r ReviewSubmissionRequest) Validate() error {
+	return util.NewValidator().Struct(r)
+}
+
+// ServiceModel creates a Submission from a given StoredSubmission.
+func ServiceModel(storedSubmission *storage.StoredSubmission) Submission {
+	return Submission{
+		Status:                 storedSubmission.Status.String(),
+		Reason:                 storedSubmission.Reason,
+		PresentationSubmission: &storedSubmission.Submission,
+	}
+}
diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go
index 7e6bf39..1061d8b 100644
--- a/pkg/service/presentation/service.go
+++ b/pkg/service/presentation/service.go
@@ -15,6 +15,7 @@ import (
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/operation"
 	opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation/model"
 	presentationstorage "github.com/tbd54566975/ssi-service/pkg/service/presentation/storage"
 	"github.com/tbd54566975/ssi-service/pkg/service/schema"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -80,7 +81,7 @@ func NewPresentationService(config config.PresentationServiceConfig, s storage.S
 
 // CreatePresentationDefinition houses the main service logic for presentation definition creation. It validates the input, and
 // produces a presentation definition value that conforms with the PresentationDefinition specification.
-func (s Service) CreatePresentationDefinition(request CreatePresentationDefinitionRequest) (*CreatePresentationDefinitionResponse, error) {
+func (s Service) CreatePresentationDefinition(request model.CreatePresentationDefinitionRequest) (*model.CreatePresentationDefinitionResponse, error) {
 	logrus.Debugf("creating presentation definition: %+v", request)
 
 	if !request.IsValid() {
@@ -97,12 +98,12 @@ func (s Service) CreatePresentationDefinition(request CreatePresentationDefiniti
 		return nil, util.LoggingErrorMsg(err, "could not store presentation")
 	}
 
-	return &CreatePresentationDefinitionResponse{
+	return &model.CreatePresentationDefinitionResponse{
 		PresentationDefinition: storedPresentation.PresentationDefinition,
 	}, nil
 }
 
-func (s Service) GetPresentationDefinition(request GetPresentationDefinitionRequest) (*GetPresentationDefinitionResponse, error) {
+func (s Service) GetPresentationDefinition(request model.GetPresentationDefinitionRequest) (*model.GetPresentationDefinitionResponse, error) {
 	logrus.Debugf("getting presentation definition: %s", request.ID)
 
 	storedPresentation, err := s.storage.GetPresentation(request.ID)
@@ -112,10 +113,10 @@ func (s Service) GetPresentationDefinition(request GetPresentationDefinitionRequ
 	if storedPresentation == nil {
 		return nil, util.LoggingNewErrorf("presentation definition with id<%s> could not be found", request.ID)
 	}
-	return &GetPresentationDefinitionResponse{ID: storedPresentation.ID, PresentationDefinition: storedPresentation.PresentationDefinition}, nil
+	return &model.GetPresentationDefinitionResponse{ID: storedPresentation.ID, PresentationDefinition: storedPresentation.PresentationDefinition}, nil
 }
 
-func (s Service) DeletePresentationDefinition(request DeletePresentationDefinitionRequest) error {
+func (s Service) DeletePresentationDefinition(request model.DeletePresentationDefinitionRequest) error {
 	logrus.Debugf("deleting presentation definition: %s", request.ID)
 
 	if err := s.storage.DeletePresentation(request.ID); err != nil {
@@ -127,7 +128,7 @@ func (s Service) DeletePresentationDefinition(request DeletePresentationDefiniti
 
 // CreateSubmission houses the main service logic for presentation submission creation. It validates the input, and
 // produces a presentation submission value that conforms with the Submission specification.
-func (s Service) CreateSubmission(request CreateSubmissionRequest) (*operation.Operation, error) {
+func (s Service) CreateSubmission(request model.CreateSubmissionRequest) (*operation.Operation, error) {
 	if !request.IsValid() {
 		return nil, errors.Errorf("invalid create presentation submission request: %+v", request)
 	}
@@ -184,7 +185,7 @@ func (s Service) CreateSubmission(request CreateSubmissionRequest) (*operation.O
 		return nil, errors.Wrap(err, "could not store presentation")
 	}
 
-	opID := fmt.Sprintf("%s/%s", opstorage.SubmissionParentResource, storedSubmission.Submission.ID)
+	opID := operation.IDFromSubmissionID(storedSubmission.Submission.ID)
 	storedOp := opstorage.StoredOperation{
 		ID:   opID,
 		Done: false,
@@ -199,22 +200,19 @@ func (s Service) CreateSubmission(request CreateSubmissionRequest) (*operation.O
 	}, nil
 }
 
-func (s Service) GetSubmission(request GetSubmissionRequest) (*GetSubmissionResponse, error) {
+func (s Service) GetSubmission(request model.GetSubmissionRequest) (*model.GetSubmissionResponse, error) {
 	logrus.Debugf("getting presentation submission: %s", request.ID)
 
 	storedSubmission, err := s.storage.GetSubmission(request.ID)
 	if err != nil {
 		return nil, errors.Wrap(err, "fetching from storage")
 	}
-	return &GetSubmissionResponse{
-		Submission: Submission{
-			Status:                 storedSubmission.Status.String(),
-			PresentationSubmission: &storedSubmission.Submission,
-		},
+	return &model.GetSubmissionResponse{
+		Submission: model.ServiceModel(storedSubmission),
 	}, nil
 }
 
-func (s Service) ListSubmissions(request ListSubmissionRequest) (*ListSubmissionResponse, error) {
+func (s Service) ListSubmissions(request model.ListSubmissionRequest) (*model.ListSubmissionResponse, error) {
 	logrus.Debug("listing presentation submissions")
 
 	subs, err := s.storage.ListSubmissions(request.Filter)
@@ -222,14 +220,25 @@ func (s Service) ListSubmissions(request ListSubmissionRequest) (*ListSubmission
 		return nil, errors.Wrap(err, "fetching submissions from storage")
 	}
 
-	resp := &ListSubmissionResponse{Submissions: make([]Submission, 0, len(subs))}
+	resp := &model.ListSubmissionResponse{Submissions: make([]model.Submission, 0, len(subs))}
 	for _, sub := range subs {
 		sub := sub // What's this?? see https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
-		resp.Submissions = append(resp.Submissions, Submission{
-			Status:                 sub.Status.String(),
-			PresentationSubmission: &sub.Submission,
-		})
+		resp.Submissions = append(resp.Submissions, model.ServiceModel(&sub))
 	}
 
 	return resp, nil
 }
+
+func (s Service) ReviewSubmission(request model.ReviewSubmissionRequest) (*model.Submission, error) {
+	if err := request.Validate(); err != nil {
+		return nil, errors.Wrap(err, "invalid request")
+	}
+
+	updatedSubmission, _, err := s.storage.UpdateSubmission(request.ID, request.Approved, request.Reason, operation.IDFromSubmissionID(request.ID))
+	if err != nil {
+		return nil, errors.Wrap(err, "updating submission")
+	}
+
+	m := model.ServiceModel(&updatedSubmission)
+	return &m, nil
+}
diff --git a/pkg/service/presentation/storage/bolt.go b/pkg/service/presentation/storage/bolt.go
index e91d1c4..6f092cb 100644
--- a/pkg/service/presentation/storage/bolt.go
+++ b/pkg/service/presentation/storage/bolt.go
@@ -6,9 +6,9 @@ import (
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/tbd54566975/ssi-service/internal/util"
-	"go.einride.tech/aip/filtering"
-
+	opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
+	"go.einride.tech/aip/filtering"
 )
 
 const (
@@ -20,6 +20,63 @@ type BoltPresentationStorage struct {
 	db *storage.BoltDB
 }
 
+type opUpdater struct {
+	storage.UpdaterWithMap
+}
+
+func (u opUpdater) SetUpdatedResponse(response []byte) {
+	u.UpdaterWithMap.Values["response"] = response
+}
+
+func (u opUpdater) Validate(v []byte) error {
+	var op opstorage.StoredOperation
+	if err := json.Unmarshal(v, &op); err != nil {
+		return errors.Wrap(err, "unmarshalling operation")
+	}
+
+	if op.Done {
+		return errors.New("operation already marked as done")
+	}
+
+	return nil
+}
+
+var _ storage.ResponseSettingUpdater = (*opUpdater)(nil)
+
+func (b BoltPresentationStorage) UpdateSubmission(id string, approved bool, reason string, opID string) (StoredSubmission, opstorage.StoredOperation, error) {
+	m := map[string]any{
+		"status": StatusDenied,
+		"reason": reason,
+	}
+	if approved {
+		m["status"] = StatusApproved
+	}
+	submissionData, operationData, err := b.db.UpdateValueAndOperation(
+		submissionNamespace,
+		id,
+		storage.NewUpdater(m),
+		opstorage.NamespaceFromID(opID),
+		opID,
+		opUpdater{
+			storage.NewUpdater(map[string]any{
+				"done": true,
+			}),
+		})
+	if err != nil {
+		return StoredSubmission{}, opstorage.StoredOperation{}, errors.Wrap(err, "updating value and operation")
+	}
+
+	var s StoredSubmission
+	if err = json.Unmarshal(submissionData, &s); err != nil {
+		return StoredSubmission{}, opstorage.StoredOperation{}, errors.Wrap(err, "unmarshalling written submission")
+	}
+	var op opstorage.StoredOperation
+	if err = json.Unmarshal(operationData, &op); err != nil {
+		return StoredSubmission{}, opstorage.StoredOperation{}, errors.Wrap(err, "unmarshalling written operation")
+	}
+	return s, op, nil
+}
+
 func (b BoltPresentationStorage) ListSubmissions(filter filtering.Filter) ([]StoredSubmission, error) {
 	allData, err := b.db.ReadAll(submissionNamespace)
 	if err != nil {
diff --git a/pkg/service/presentation/storage/storage.go b/pkg/service/presentation/storage/storage.go
index 35099e5..41b2488 100644
--- a/pkg/service/presentation/storage/storage.go
+++ b/pkg/service/presentation/storage/storage.go
@@ -4,6 +4,7 @@ import (
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
 	"github.com/pkg/errors"
 	"github.com/tbd54566975/ssi-service/internal/util"
+	opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 	"go.einride.tech/aip/filtering"
 )
@@ -47,10 +48,12 @@ type Status uint8
 
 func (s Status) String() string {
 	switch s {
-	case StatusDone:
-		return "done"
 	case StatusPending:
 		return "pending"
+	case StatusDenied:
+		return "denied"
+	case StatusApproved:
+		return "approved"
 	default:
 		return "unknown"
 	}
@@ -59,12 +62,14 @@ func (s Status) String() string {
 const (
 	StatusUnknown Status = iota
 	StatusPending
-	StatusDone
+	StatusDenied
+	StatusApproved
 )
 
 type StoredSubmission struct {
 	Status     Status                          `json:"status"`
 	Submission exchange.PresentationSubmission `json:"submission"`
+	Reason     string                          `json:"reason"`
 }
 
 func (s StoredSubmission) FilterVariablesMap() map[string]interface{} {
@@ -77,6 +82,7 @@ type SubmissionStorage interface {
 	StoreSubmission(schema StoredSubmission) error
 	GetSubmission(id string) (*StoredSubmission, error)
 	ListSubmissions(filtering.Filter) ([]StoredSubmission, error)
+	UpdateSubmission(id string, approved bool, reason string, submissionID string) (StoredSubmission, opstorage.StoredOperation, error)
 }
 
 var ErrSubmissionNotFound = errors.New("submission not found")
diff --git a/pkg/storage/bolt.go b/pkg/storage/bolt.go
index ab81513..827b8fa 100644
--- a/pkg/storage/bolt.go
+++ b/pkg/storage/bolt.go
@@ -3,6 +3,7 @@ package storage
 import (
 	"bytes"
 	"fmt"
+	"github.com/goccy/go-json"
 	"strings"
 	"time"
 
@@ -141,6 +142,108 @@ func (b *BoltDB) DeleteNamespace(namespace string) error {
 	})
 }
 
+// UpdaterWithMap is a json map based Updater implementation. The key/values from the map are used to update the
+// unmarshalled JSON representation of the stored data.
+type UpdaterWithMap struct {
+	Values map[string]any
+}
+
+// Validate is a default implementation for UpdaterWithMap which does no validation. Users can pass embed UpdaterWithMap
+// into a custom struct and redefine this method in order to have custom logic.
+func (u UpdaterWithMap) Validate(v []byte) error {
+	return nil
+}
+
+// NewUpdater creates a new UpdaterWithMap with the given map.
+func NewUpdater(values map[string]any) UpdaterWithMap {
+	return UpdaterWithMap{
+		Values: values,
+	}
+}
+
+func (u UpdaterWithMap) Update(v []byte) ([]byte, error) {
+	var model map[string]interface{}
+	if err := json.Unmarshal(v, &model); err != nil {
+		return nil, errors.Wrap(err, "unmarshalling json")
+	}
+	for k, val := range u.Values {
+		model[k] = val
+	}
+	data, err := json.Marshal(model)
+	if err != nil {
+		return nil, errors.Wrap(err, "marshalling updated struct")
+	}
+	return data, nil
+}
+
+// Updater encapsulates the Update method, which take a slice of bytes, and updates it before it's stored in the DB.
+type Updater interface {
+	Update(v []byte) ([]byte, error)
+	// Validate runs after the data has been loaded from disk, but before the write is actually performed.
+	Validate(v []byte) error
+}
+
+type ResponseSettingUpdater interface {
+	Updater
+	// SetUpdatedResponse sets the response that the Update method will later use to modify the data.
+	SetUpdatedResponse([]byte)
+}
+
+// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map.
+// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true.
+func (b *BoltDB) UpdateValueAndOperation(namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) {
+	err = b.db.Update(func(tx *bolt.Tx) error {
+		if err := updateTxFn(namespace, key, updater, &first)(tx); err != nil {
+			return err
+		}
+		opUpdater.SetUpdatedResponse(first)
+		if err = updateTxFn(opNamespace, opKey, opUpdater, &op)(tx); err != nil {
+			return err
+		}
+		return nil
+	})
+	return first, op, err
+}
+
+func (b *BoltDB) Update(namespace string, key string, values map[string]any) ([]byte, error) {
+	var updatedData []byte
+	err := b.db.Update(updateTxFn(namespace, key, NewUpdater(values), &updatedData))
+	return updatedData, err
+}
+
+func updateTxFn(namespace string, key string, updater Updater, updatedData *[]byte) func(tx *bolt.Tx) error {
+	return func(tx *bolt.Tx) error {
+		data, err := updateTx(tx, namespace, key, updater)
+		if err != nil {
+			return err
+		}
+		*updatedData = data
+		return nil
+	}
+}
+
+func updateTx(tx *bolt.Tx, namespace string, key string, updater Updater) ([]byte, error) {
+	bucket := tx.Bucket([]byte(namespace))
+	if bucket == nil {
+		return nil, util.LoggingNewErrorf("namespace<%s> does not exist", namespace)
+	}
+	v := bucket.Get([]byte(key))
+	if v == nil {
+		return nil, util.LoggingNewErrorf("key not found %s", key)
+	}
+	if err := updater.Validate(v); err != nil {
+		return nil, util.LoggingErrorMsg(err, "validating update")
+	}
+	data, err := updater.Update(v)
+	if err != nil {
+		return nil, err
+	}
+	if err = bucket.Put([]byte(key), data); err != nil {
+		return nil, errors.Wrap(err, "writing to db")
+	}
+	return data, nil
+}
+
 // MakeNamespace takes a set of possible namespace values and combines them as a convention
 func MakeNamespace(ns ...string) string {
 	return strings.Join(ns, "-")
diff --git a/sip/sips/sip6/README.md b/sip/sips/sip6/README.md
index 7432f50..2215525 100644
--- a/sip/sips/sip6/README.md
+++ b/sip/sips/sip6/README.md
@@ -246,7 +246,7 @@ Upon a submission, the following steps will be performed:
 9. Check that the paths are resolvable to fields in the claims. This should follow the spec as described in [https://identity.foundation/presentation-exchange/#processing-of-submission-entries](https://identity.foundation/presentation-exchange/#processing-of-submission-entries)
 10. A `Submission` is stored in the DB.
 11. An operation ID is generated, and an operation is stored in a KV database.
-12. A response of type `Operation` is sent back to the client with `id := "presentations/submissions/{submission_id}"`
+12. A response of type `Operation` is sent back to the client with `id := "/presentations/submissions/{submission_id}"`
 13. Bask in the glory of a successful submission.
 
 ### GET
@@ -280,7 +280,7 @@ The URL is `/v1/presentations/submissions/:id/review`. This endpoint enables adm
 }
 ```
 
-Ideally, only the `TBD Admin` should have authorization to perform this. After this method is called, the operation with `id==presentations/submissions/{submission_id}` will be updated with the result of this invocation (and the `done` field will be set to true). The `submission` object’s review state will be updated as well.
+Ideally, only the `TBD Admin` should have authorization to perform this. After this method is called, the operation with `id==/presentations/submissions/{submission_id}` will be updated with the result of this invocation (and the `done` field will be set to true). The `submission` object’s review state will be updated as well.
 
 The response of this endpoint will contain a `Submission` object with `status != 'pending'`.
 
@@ -292,7 +292,7 @@ An object of type `Operation` and will look as follows
 
 ```json
 {
-  "id": "{namespace}/{unique_id}", // example: "presentations/submissions/a30e3b91-fb77-4d22-95fa-871689c322e2"
+  "id": "/{namespace}/{unique_id}", // example: "/presentations/submissions/a30e3b91-fb77-4d22-95fa-871689c322e2"
   "done": true, // when "false", then "result" will be empty as it's still being calculated.
   "result": { // only present when "done" == true. When present only one of ["error", "response"] will be populated.
     "error": "some string with the error",


Test patch:
diff --git a/pkg/server/router/presentation_test.go b/pkg/server/router/presentation_test.go
index 3f51be5..2a70522 100644
--- a/pkg/server/router/presentation_test.go
+++ b/pkg/server/router/presentation_test.go
@@ -6,6 +6,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation/model"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 	"os"
 	"testing"
@@ -44,17 +45,17 @@ func TestPresentationDefinitionService(t *testing.T) {
 
 	t.Run("Create returns the created definition", func(t *testing.T) {
 		pd := createPresentationDefinition(t)
-		created, err := service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
+		created, err := service.CreatePresentationDefinition(model.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
 		assert.NoError(t, err)
 		assert.Equal(t, pd, &created.PresentationDefinition)
 	})
 
 	t.Run("Get returns the created definition", func(t *testing.T) {
 		pd := createPresentationDefinition(t)
-		_, err := service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
+		_, err := service.CreatePresentationDefinition(model.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
 		assert.NoError(t, err)
 
-		getPd, err := service.GetPresentationDefinition(presentation.GetPresentationDefinitionRequest{ID: pd.ID})
+		getPd, err := service.GetPresentationDefinition(model.GetPresentationDefinitionRequest{ID: pd.ID})
 
 		assert.NoError(t, err)
 		assert.Equal(t, pd.ID, getPd.ID)
@@ -63,17 +64,17 @@ func TestPresentationDefinitionService(t *testing.T) {
 
 	t.Run("Get does not return after deletion", func(t *testing.T) {
 		pd := createPresentationDefinition(t)
-		_, err := service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
+		_, err := service.CreatePresentationDefinition(model.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
 		assert.NoError(t, err)
 
-		assert.NoError(t, service.DeletePresentationDefinition(presentation.DeletePresentationDefinitionRequest{ID: pd.ID}))
+		assert.NoError(t, service.DeletePresentationDefinition(model.DeletePresentationDefinitionRequest{ID: pd.ID}))
 
-		_, err = service.GetPresentationDefinition(presentation.GetPresentationDefinitionRequest{ID: pd.ID})
+		_, err = service.GetPresentationDefinition(model.GetPresentationDefinitionRequest{ID: pd.ID})
 		assert.Error(t, err)
 	})
 
 	t.Run("Delete can be called with any ID", func(t *testing.T) {
-		err := service.DeletePresentationDefinition(presentation.DeletePresentationDefinitionRequest{ID: "some crazy ID"})
+		err := service.DeletePresentationDefinition(model.DeletePresentationDefinitionRequest{ID: "some crazy ID"})
 		assert.NoError(t, err)
 	})
 }
diff --git a/pkg/server/server_operation_test.go b/pkg/server/server_operation_test.go
index 99235ee..aca2abc 100644
--- a/pkg/server/server_operation_test.go
+++ b/pkg/server/server_operation_test.go
@@ -1,11 +1,13 @@
 package server
 
 import (
+	"fmt"
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
 	"github.com/goccy/go-json"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"github.com/tbd54566975/ssi-service/pkg/server/router"
 	"github.com/tbd54566975/ssi-service/pkg/service/operation"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -16,10 +18,83 @@ import (
 )
 
 func TestOperationsAPI(t *testing.T) {
+	t.Run("Marks operation as done after reviewing submission", func(t *testing.T) {
+		s := setupTestDB(t)
+		pRouter := setupPresentationRouter(t, s)
+		opRouter := setupOperationsRouter(t, s)
+
+		holderSigner, holderDID := getSigner(t)
+		definition := createPresentationDefinition(t, pRouter)
+		submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner)
+		submission := reviewSubmission(t, pRouter, operation.SubmissionID(submissionOp.ID))
+
+		createdID := submissionOp.ID
+		req := httptest.NewRequest(
+			http.MethodPut,
+			fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID),
+			nil)
+		w := httptest.NewRecorder()
+
+		err := opRouter.GetOperation(newRequestContextWithParams(map[string]string{"id": createdID}), w, req)
+
+		assert.NoError(t, err)
+		var resp router.Operation
+		assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+		assert.True(t, resp.Done)
+		assert.Empty(t, resp.Result.Error)
+		data, err := json.Marshal(submission)
+		assert.NoError(t, err)
+		var responseAsMap map[string]any
+		assert.NoError(t, json.Unmarshal(data, &responseAsMap))
+		assert.Equal(t, responseAsMap, resp.Result.Response)
+	})
+
+	t.Run("GetOperation", func(t *testing.T) {
+		t.Run("Returns operation after submission", func(t *testing.T) {
+			s := setupTestDB(t)
+			pRouter := setupPresentationRouter(t, s)
+			opRouter := setupOperationsRouter(t, s)
+
+			holderSigner, holderDID := getSigner(t)
+			definition := createPresentationDefinition(t, pRouter)
+			submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner)
+
+			createdID := submissionOp.ID
+			req := httptest.NewRequest(
+				http.MethodPut,
+				fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID),
+				nil)
+			w := httptest.NewRecorder()
+
+			err := opRouter.GetOperation(newRequestContextWithParams(map[string]string{"id": createdID}), w, req)
+
+			assert.NoError(t, err)
+			var resp router.Operation
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.False(t, resp.Done)
+			assert.Contains(t, resp.ID, "/presentations/submissions/")
+		})
+
+		t.Run("Returns error when id doesn't exist", func(t *testing.T) {
+			s := setupTestDB(t)
+			opRouter := setupOperationsRouter(t, s)
+
+			req := httptest.NewRequest(
+				http.MethodPut,
+				"https://ssi-service.com/v1/operations/some_fake_id",
+				nil)
+			w := httptest.NewRecorder()
+
+			err := opRouter.GetOperation(newRequestContextWithParams(map[string]string{"id": "some_fake_id"}), w, req)
+
+			assert.Error(t, err)
+			assert.Contains(t, err.Error(), "operation not found with id")
+		})
+	})
+
 	t.Run("GetOperations", func(t *testing.T) {
 		t.Run("Returns empty when no operations stored", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			opRouter := setupOperationsRouter(t, s)
 
 			request := router.GetOperationsRequest{
@@ -37,8 +112,7 @@ func TestOperationsAPI(t *testing.T) {
 		})
 
 		t.Run("Returns one operation for every submission", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 			opRouter := setupOperationsRouter(t, s)
 
@@ -73,8 +147,7 @@ func TestOperationsAPI(t *testing.T) {
 		})
 
 		t.Run("Returns operation when filtering to include", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 			opRouter := setupOperationsRouter(t, s)
 
@@ -99,8 +172,7 @@ func TestOperationsAPI(t *testing.T) {
 		})
 
 		t.Run("Returns zero operations when filtering to exclude", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 			opRouter := setupOperationsRouter(t, s)
 
@@ -124,8 +196,7 @@ func TestOperationsAPI(t *testing.T) {
 		})
 
 		t.Run("Returns zero operations when wrong parent is specified", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 			opRouter := setupOperationsRouter(t, s)
 
@@ -150,11 +221,41 @@ func TestOperationsAPI(t *testing.T) {
 
 }
 
-func setupOperationsRouter(t *testing.T, s storage.ServiceStorage) *router.OperationRouter {
+func setupTestDB(t *testing.T) storage.ServiceStorage {
+	file, err := os.CreateTemp("", "bolt")
+	require.NoError(t, err)
+	name := file.Name()
+	s, err := storage.NewBoltDBWithFile(name)
+	require.NoError(t, err)
 	t.Cleanup(func() {
-		assert.NoError(t, s.Close())
-		assert.NoError(t, os.Remove(storage.DBFile))
+		_ = s.Close()
+		_ = os.Remove(name)
 	})
+	return s
+}
+
+func reviewSubmission(t *testing.T, pRouter *router.PresentationRouter, submissionID string) router.ReviewSubmissionResponse {
+	request := router.ReviewSubmissionRequest{
+		Approved: true,
+		Reason:   "because I want to",
+	}
+
+	value := newRequestValue(t, request)
+	req := httptest.NewRequest(
+		http.MethodPut,
+		fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", submissionID),
+		value)
+	w := httptest.NewRecorder()
+
+	err := pRouter.ReviewSubmission(newRequestContextWithParams(map[string]string{"id": submissionID}), w, req)
+
+	assert.NoError(t, err)
+	var resp router.ReviewSubmissionResponse
+	assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+	return resp
+}
+
+func setupOperationsRouter(t *testing.T, s storage.ServiceStorage) *router.OperationRouter {
 	svc, err := operation.NewOperationService(s)
 	assert.NoError(t, err)
 	opRouter, err := router.NewOperationRouter(svc)
diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go
index fad3665..b754b21 100644
--- a/pkg/server/server_presentation_test.go
+++ b/pkg/server/server_presentation_test.go
@@ -18,10 +18,10 @@ import (
 	"github.com/tbd54566975/ssi-service/pkg/server/router"
 	"github.com/tbd54566975/ssi-service/pkg/service/operation"
 	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation/model"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 	"net/http"
 	"net/http/httptest"
-	"os"
 	"testing"
 )
 
@@ -45,8 +45,7 @@ func TestPresentationAPI(t *testing.T) {
 	assert.NoError(t, err)
 
 	t.Run("Create, Get, and Delete PresentationDefinition", func(t *testing.T) {
-		s, err := storage.NewBoltDB()
-		assert.NoError(t, err)
+		s := setupTestDB(t)
 		pRouter := setupPresentationRouter(t, s)
 
 		var createdID string
@@ -106,8 +105,7 @@ func TestPresentationAPI(t *testing.T) {
 	})
 
 	t.Run("Create returns error without input descriptors", func(t *testing.T) {
-		s, err := storage.NewBoltDB()
-		assert.NoError(t, err)
+		s := setupTestDB(t)
 		pRouter := setupPresentationRouter(t, s)
 		request := router.CreatePresentationDefinitionRequest{}
 		value := newRequestValue(t, request)
@@ -120,8 +118,7 @@ func TestPresentationAPI(t *testing.T) {
 	})
 
 	t.Run("Get without an ID returns error", func(t *testing.T) {
-		s, err := storage.NewBoltDB()
-		assert.NoError(t, err)
+		s := setupTestDB(t)
 		pRouter := setupPresentationRouter(t, s)
 		req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil)
 		w := httptest.NewRecorder()
@@ -130,8 +127,7 @@ func TestPresentationAPI(t *testing.T) {
 	})
 
 	t.Run("Delete without an ID returns error", func(t *testing.T) {
-		s, err := storage.NewBoltDB()
-		assert.NoError(t, err)
+		s := setupTestDB(t)
 		pRouter := setupPresentationRouter(t, s)
 
 		req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil)
@@ -143,8 +139,7 @@ func TestPresentationAPI(t *testing.T) {
 	t.Run("Submission endpoints", func(t *testing.T) {
 
 		t.Run("Get non-existing ID returns error", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions/myrandomid", nil)
@@ -153,8 +148,7 @@ func TestPresentationAPI(t *testing.T) {
 		})
 
 		t.Run("Get returns submission after creation", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			holderSigner, holderDID := getSigner(t)
@@ -181,8 +175,7 @@ func TestPresentationAPI(t *testing.T) {
 		})
 
 		t.Run("Create well formed submission returns operation", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			holderSigner, holderDID := getSigner(t)
@@ -210,9 +203,68 @@ func TestPresentationAPI(t *testing.T) {
 			assert.Zero(t, resp.Result)
 		})
 
-		t.Run("List submissions returns empty when there are none", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
+		t.Run("Review submission returns approved submission", func(t *testing.T) {
+			s := setupTestDB(t)
+			pRouter := setupPresentationRouter(t, s)
+
+			holderSigner, holderDID := getSigner(t)
+			definition := createPresentationDefinition(t, pRouter)
+			submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner)
+
+			request := router.ReviewSubmissionRequest{
+				Approved: true,
+				Reason:   "because I want to",
+			}
+
+			value := newRequestValue(t, request)
+			createdID := operation.SubmissionID(submissionOp.ID)
+			req := httptest.NewRequest(
+				http.MethodPut,
+				fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID),
+				value)
+			w := httptest.NewRecorder()
+
+			err = pRouter.ReviewSubmission(newRequestContextWithParams(map[string]string{"id": createdID}), w, req)
+
 			assert.NoError(t, err)
+			var resp router.ReviewSubmissionResponse
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.Equal(t, "because I want to", resp.Reason)
+			assert.NotEmpty(t, resp.ID)
+			assert.Equal(t, "approved", resp.Status)
+			assert.Equal(t, definition.PresentationDefinition.ID, resp.DefinitionID)
+		})
+
+		t.Run("Review submission twice fails", func(t *testing.T) {
+			s := setupTestDB(t)
+			pRouter := setupPresentationRouter(t, s)
+
+			holderSigner, holderDID := getSigner(t)
+			definition := createPresentationDefinition(t, pRouter)
+			submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner)
+			createdID := operation.SubmissionID(submissionOp.ID)
+			_ = reviewSubmission(t, pRouter, createdID)
+
+			request := router.ReviewSubmissionRequest{
+				Approved: true,
+				Reason:   "because I want to review again",
+			}
+
+			value := newRequestValue(t, request)
+			req := httptest.NewRequest(
+				http.MethodPut,
+				fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID),
+				value)
+			w := httptest.NewRecorder()
+
+			err = pRouter.ReviewSubmission(newRequestContextWithParams(map[string]string{"id": createdID}), w, req)
+
+			assert.Error(t, err)
+			assert.Contains(t, err.Error(), "operation already marked as done")
+		})
+
+		t.Run("List submissions returns empty when there are none", func(t *testing.T) {
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			request := router.ListSubmissionRequest{}
@@ -230,8 +282,7 @@ func TestPresentationAPI(t *testing.T) {
 		})
 
 		t.Run("List submissions returns many submissions", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			holderSigner, holderDID := getSigner(t)
@@ -267,7 +318,7 @@ func TestPresentationAPI(t *testing.T) {
 			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
 			assert.Len(t, resp.Submissions, 2)
 
-			expectedSubmissions := []presentation.Submission{
+			expectedSubmissions := []model.Submission{
 				{
 					Status: "pending",
 					PresentationSubmission: &exchange.PresentationSubmission{
@@ -285,7 +336,7 @@ func TestPresentationAPI(t *testing.T) {
 			}
 			diff := cmp.Diff(expectedSubmissions, resp.Submissions,
 				cmpopts.IgnoreFields(exchange.PresentationSubmission{}, "DescriptorMap"),
-				cmpopts.SortSlices(func(l, r presentation.Submission) bool {
+				cmpopts.SortSlices(func(l, r model.Submission) bool {
 					return l.PresentationSubmission.ID < r.PresentationSubmission.ID
 				}),
 			)
@@ -295,8 +346,7 @@ func TestPresentationAPI(t *testing.T) {
 		})
 
 		t.Run("bad filter returns error", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 			request := router.ListSubmissionRequest{
 				Filter: `im a baaad filter that's trying to break a lot of stuff'`,
@@ -312,8 +362,7 @@ func TestPresentationAPI(t *testing.T) {
 		})
 
 		t.Run("List submissions filters based on status", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			holderSigner, holderDID := getSigner(t)
@@ -341,7 +390,7 @@ func TestPresentationAPI(t *testing.T) {
 			var resp router.ListSubmissionResponse
 			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
 
-			expectedSubmissions := []presentation.Submission{
+			expectedSubmissions := []model.Submission{
 				{
 					Status: "pending",
 					PresentationSubmission: &exchange.PresentationSubmission{
@@ -352,7 +401,7 @@ func TestPresentationAPI(t *testing.T) {
 			}
 			diff := cmp.Diff(expectedSubmissions, resp.Submissions,
 				cmpopts.IgnoreFields(exchange.PresentationSubmission{}, "DescriptorMap"),
-				cmpopts.SortSlices(func(l, r presentation.Submission) bool {
+				cmpopts.SortSlices(func(l, r model.Submission) bool {
 					return l.PresentationSubmission.ID < r.PresentationSubmission.ID
 				}),
 			)
@@ -362,8 +411,7 @@ func TestPresentationAPI(t *testing.T) {
 		})
 
 		t.Run("List submissions filter returns empty when status does not match", func(t *testing.T) {
-			s, err := storage.NewBoltDB()
-			assert.NoError(t, err)
+			s := setupTestDB(t)
 			pRouter := setupPresentationRouter(t, s)
 
 			holderSigner, holderDID := getSigner(t)
@@ -407,11 +455,6 @@ func setupPresentationRouter(t *testing.T, s storage.ServiceStorage) *router.Pre
 
 	pRouter, err := router.NewPresentationRouter(service)
 	assert.NoError(t, err)
-
-	t.Cleanup(func() {
-		_ = s.Close()
-		_ = os.Remove(storage.DBFile)
-	})
 	return pRouter
 }
 
diff --git a/pkg/storage/bolt_test.go b/pkg/storage/bolt_test.go
index e46a48b..c816eaa 100644
--- a/pkg/storage/bolt_test.go
+++ b/pkg/storage/bolt_test.go
@@ -1,21 +1,16 @@
 package storage
 
 import (
+	"fmt"
 	"github.com/goccy/go-json"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"os"
 	"testing"
 )
 
 func TestBoltDB(t *testing.T) {
-	db, err := NewBoltDBWithFile("test.db")
-	assert.NoError(t, err)
-	assert.NotEmpty(t, db)
-
-	t.Cleanup(func() {
-		_ = db.Close()
-		os.Remove("test.db")
-	})
+	db := setupBoltDB(t)
 
 	// create a name space and a message in it
 	namespace := "F1"
@@ -94,21 +89,14 @@ func TestBoltDB(t *testing.T) {
 }
 
 func TestBoltDBPrefixAndKeys(t *testing.T) {
-	db, err := NewBoltDBWithFile("test.db")
-	assert.NoError(t, err)
-	assert.NotEmpty(t, db)
-
-	t.Cleanup(func() {
-		_ = db.Close()
-		os.Remove("test.db")
-	})
+	db := setupBoltDB(t)
 
 	namespace := "blockchains"
 
 	// set up prefix read test
 
 	dummyData := []byte("dummy")
-	err = db.Write(namespace, "bitcoin-testnet", dummyData)
+	err := db.Write(namespace, "bitcoin-testnet", dummyData)
 	assert.NoError(t, err)
 
 	err = db.Write(namespace, "bitcoin-mainnet", dummyData)
@@ -140,3 +128,211 @@ func TestBoltDBPrefixAndKeys(t *testing.T) {
 	assert.Contains(t, allKeys, "bitcoin-mainnet")
 	assert.Contains(t, allKeys, "tezos-mainnet")
 }
+
+func setupBoltDB(t *testing.T) *BoltDB {
+	db, err := NewBoltDBWithFile("test.db")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, db)
+
+	t.Cleanup(func() {
+		_ = db.Close()
+		os.Remove("test.db")
+	})
+	return db
+}
+
+type testStruct struct {
+	Status int    `json:"status"`
+	Reason string `json:"reason"`
+}
+
+type operation struct {
+	Done     bool   `json:"done"`
+	Response []byte `json:"response"`
+}
+
+func TestBoltDB_Update(t *testing.T) {
+	db := setupBoltDB(t)
+	namespace := "simple"
+
+	data, err := json.Marshal(testStruct{
+		Status: 0,
+		Reason: "",
+	})
+	require.NoError(t, err)
+	require.NoError(t, db.Write(namespace, "123", data))
+
+	type args struct {
+		key    string
+		values map[string]any
+	}
+	tests := []struct {
+		name          string
+		args          args
+		expectedData  testStruct
+		expectedError assert.ErrorAssertionFunc
+	}{
+		{
+			name: "simple update",
+			args: args{
+				key: "123",
+				values: map[string]any{
+					"status": 1,
+					"reason": "something here",
+				},
+			},
+			expectedData: testStruct{
+				Status: 1,
+				Reason: "something here",
+			},
+			expectedError: assert.NoError,
+		},
+		{
+			name: "other key returns error",
+			args: args{
+				key: "456",
+				values: map[string]any{
+					"status": 1,
+					"reason": "something here",
+				},
+			},
+			expectedData:  testStruct{},
+			expectedError: assert.Error,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			data, err = db.Update(namespace, tt.args.key, tt.args.values)
+			if !tt.expectedError(t, err) {
+				return
+			}
+			var s testStruct
+			if tt.expectedData != s {
+				assert.NoError(t, json.Unmarshal(data, &s))
+				assert.Equal(t, tt.expectedData, s)
+			}
+		})
+	}
+}
+
+type testOpUpdater struct {
+	UpdaterWithMap
+}
+
+func (f testOpUpdater) SetUpdatedResponse(bytes []byte) {
+	f.UpdaterWithMap.Values["response"] = bytes
+}
+
+func TestBoltDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) {
+	db := setupBoltDB(t)
+	namespace := "simple"
+	opNamespace := "operation"
+
+	data, err := json.Marshal(testStruct{
+		Status: 0,
+		Reason: "",
+	})
+	require.NoError(t, err)
+	require.NoError(t, db.Write(namespace, "123", data))
+
+	data, err = json.Marshal(operation{
+		Done:     false,
+		Response: nil,
+	})
+	require.NoError(t, err)
+	require.NoError(t, db.Write(opNamespace, "op123", data))
+
+	type args struct {
+		namespace   string
+		key         string
+		updater     Updater
+		opNamespace string
+		opKey       string
+	}
+	tests := []struct {
+		name           string
+		args           args
+		wantFirst      *testStruct
+		wantOpDone     bool
+		wantOpResponse *testStruct
+		wantErr        assert.ErrorAssertionFunc
+	}{
+		{
+			name: "first and second get updated",
+			args: args{
+				namespace: namespace,
+				key:       "123",
+				updater: NewUpdater(map[string]any{
+					"status": 1,
+					"reason": "hello",
+				}),
+				opNamespace: opNamespace,
+				opKey:       "op123",
+			},
+			wantFirst: &testStruct{
+				Status: 1,
+				Reason: "hello",
+			},
+			wantOpDone: true,
+			wantOpResponse: &testStruct{
+				Status: 1,
+				Reason: "hello",
+			},
+			wantErr: assert.NoError,
+		},
+		{
+			name: "non-existent op key returns error",
+			args: args{
+				namespace: namespace,
+				key:       "123",
+				updater: NewUpdater(map[string]any{
+					"status": 1,
+					"reason": "hello",
+				}),
+				opNamespace: opNamespace,
+				opKey:       "crazy key",
+			},
+			wantErr: assert.Error,
+		},
+		{
+			name: "non-existent key returns error",
+			args: args{
+				namespace: namespace,
+				key:       "crazy key",
+				updater: NewUpdater(map[string]any{
+					"status": 1,
+					"reason": "hello",
+				}),
+				opNamespace: opNamespace,
+				opKey:       "op123",
+			},
+			wantErr: assert.Error,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotFirstData, gotOpData, err := db.UpdateValueAndOperation(tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{
+				NewUpdater(map[string]any{
+					"done": true,
+				}),
+			})
+			if !tt.wantErr(t, err, fmt.Sprintf("UpdateValueAndOperation(%v, %v, %v, %v, %v)", tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey)) {
+				return
+			}
+			if tt.wantFirst == nil {
+				return
+			}
+			var gotFirst testStruct
+			assert.NoError(t, json.Unmarshal(gotFirstData, &gotFirst))
+			assert.Equal(t, *tt.wantFirst, gotFirst)
+
+			var gotOp operation
+			assert.NoError(t, json.Unmarshal(gotOpData, &gotOp))
+			assert.Equal(t, tt.wantOpDone, gotOp.Done)
+
+			var gotOpResponse testStruct
+			assert.NoError(t, json.Unmarshal(gotOp.Response, &gotOpResponse))
+			assert.Equal(t, *tt.wantOpResponse, gotOpResponse)
+		})
+	}
+}