public CompletableFuture<Boolean> put(
      @Nonnull final ReceiptSerial receiptSerial,
      final long receiptExpiration,
      final long receiptLevel,
      @Nonnull final UUID accountUuid) {

    // fail early if given bad inputs
    Objects.requireNonNull(receiptSerial);
    Objects.requireNonNull(accountUuid);

    final Instant now = clock.instant();
    final Instant rowExpiration = now.plus(expirationTime);
    final AttributeValue serialAttributeValue = AttributeValues.b(receiptSerial.serialize());

    final UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
        .tableName(table)
        .key(Map.of(KEY_SERIAL, serialAttributeValue))
        .returnValues(ReturnValue.ALL_NEW)
        .updateExpression("SET #ttl = if_not_exists(#ttl, :ttl), "
            + "#receipt_expiration = if_not_exists(#receipt_expiration, :receipt_expiration), "
            + "#receipt_level = if_not_exists(#receipt_level, :receipt_level), "
            + "#account_uuid = if_not_exists(#account_uuid, :account_uuid), "
            + "#redemption_time = if_not_exists(#redemption_time, :redemption_time)")
        .expressionAttributeNames(Map.of(
            "#ttl", KEY_TTL,
            "#receipt_expiration", KEY_RECEIPT_EXPIRATION,
            "#receipt_level", KEY_RECEIPT_LEVEL,
            "#account_uuid", KEY_ACCOUNT_UUID,
            "#redemption_time", KEY_REDEMPTION_TIME))
        .expressionAttributeValues(Map.of(
            ":ttl", AttributeValues.n(rowExpiration.getEpochSecond()),
            ":receipt_expiration", AttributeValues.n(receiptExpiration),
            ":receipt_level", AttributeValues.n(receiptLevel),
            ":account_uuid", AttributeValues.b(accountUuid),
            ":redemption_time", AttributeValues.n(now.getEpochSecond())))
        .build();
    return client.updateItem(updateItemRequest).thenApply(updateItemResponse -> {
      final Map<String, AttributeValue> attributes = updateItemResponse.attributes();
      final long ddbReceiptExpiration = Long.parseLong(attributes.get(KEY_RECEIPT_EXPIRATION).n());
      final long ddbReceiptLevel = Long.parseLong(attributes.get(KEY_RECEIPT_LEVEL).n());
      final UUID ddbAccountUuid = UUIDUtil.fromByteBuffer(attributes.get(KEY_ACCOUNT_UUID).b().asByteBuffer());
      return ddbReceiptExpiration == receiptExpiration && ddbReceiptLevel == receiptLevel &&
          Objects.equals(ddbAccountUuid, accountUuid);
    });
  }
@Test
  void testPut() throws ExecutionException, InterruptedException {
    final long receiptExpiration = 42;
    final long receiptLevel = 3;
    CompletableFuture<Boolean> put;

    // initial insert should return true
    put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID);
    assertThat(put.get()).isTrue();

    // subsequent attempted inserts with modified parameters should return false
    put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration + 1, receiptLevel, AuthHelper.VALID_UUID);
    assertThat(put.get()).isFalse();
    put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel + 1, AuthHelper.VALID_UUID);
    assertThat(put.get()).isFalse();
    put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID_TWO);
    assertThat(put.get()).isFalse();

    // repeated insert attempt of the original parameters should return true
    put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID);
    assertThat(put.get()).isTrue();
  }