メインコンテンツまでスキップ

lesson-2_予約機能を完成させよう

🖋 予約機能を完成させよう

このレッスンでは、予約機能を完成させるためにChangeメソッドを追加していきます。

まずは、宿泊者が部屋を予約するためのメソッドを実装していきます。

以下のコードを追加しましょう。existsメソッドの下に追加すると良いでしょう。

/contract/src/lib.rs

    pub fn exists(&self, owner_id: AccountId, room_name: String) -> bool {
...
}

+ // 部屋を予約する
+ #[payable]
+ pub fn book_room(&mut self, room_id: RoomId, check_in_date: CheckInDate) {
+ let room = self
+ .rooms_by_id
+ .get_mut(&room_id)
+ .expect("ERR_NOT_FOUND_ROOM");
+
+ let account_id = env::signer_account_id();
+
+ // 関数コール時に送付されたNEARを取得
+ let deposit = env::attached_deposit();
+ // 送付されたNEARと実際の宿泊料(NEAR)を比較するためにキャストをする
+ let room_price: u128 = room.price.into();
+ assert_eq!(deposit, room_price, "ERR_DEPOSIT_IS_INCORRECT");
+
+ // 予約が入った日付, 宿泊者IDを登録
+ room.booked_info
+ .insert(check_in_date.clone(), account_id.clone());
+
+ // 宿泊者に予約データを保存
+ let owner_id = room.owner_id.clone();
+ self.add_booking_to_guest(account_id, room_id, check_in_date);
+
+ // 宿泊料を部屋のオーナーへ支払う
+ Promise::new(owner_id).transfer(deposit);
+ }

pub fn get_rooms_registered_by_owner(&self, owner_id: AccountId) -> Vec<ResigteredRoom> {
...
}

追加した内容を見ていきましょう。

book_roomメソッドでは、宿泊データを保存し実際に宿泊料をオーナーへ送信します。このメソッドには、#[payable]アノテーションが指定されています。これをメソッドに指定することで、呼び出しとトークンの転送ができるようになります。

まずは、予約される部屋のデータを取得します。ここで、データの取得にget_mut()を使用していることに注目してください。mutmutableの意味で、変更可能なroomが取得できます。予約データを追加し、roomを更新したいので可変のデータが取得できるget_mut()を使います。

        let room = self
.rooms_by_id
.get_mut(&room_id)
.expect("ERR_NOT_FOUND_ROOM");

env::attached_deposit()メソッドで、宿泊者が支払うNEARを取得します。

        // 関数コール時に送付されたNEARを取得
let deposit = env::attached_deposit();
// 送付されたNEARと実際の宿泊料(NEAR)を比較するためにキャストをする
let room_price: u128 = room.price.into();
assert_eq!(deposit, room_price, "ERR_DEPOSIT_IS_INCORRECT");

取得したdepositの型はU128です。depositがオーナーの掲載する宿泊料と等しいか比較をするために、u128に型変換をしています。変換にはinto()メソッドを使用しています。 assert_eq!()メソッドで比較をし、もし等しくなければエラーとなります。

次に、予約データをブロックチェーン上へ保存します。中で呼んでいるadd_booking_to_guestメソッドは、後ほど実装します。

        // 予約が入った日付, 宿泊者IDを登録
room.booked_info
.insert(check_in_date.clone(), account_id.clone());

// 宿泊者に予約データを保存
let owner_id = room.owner_id.clone();
self.add_booking_to_guest(account_id, room_id, check_in_date);

最後に、宿泊料をオーナーへ支払います。transfer()メソッドは、引数に受け取ったトークンをPromiseが動作するアカウントに転送します。Promiseは、オーナーのアカウントIDを指定して作成されていることに注目してください。ここでは、「Promiseが動作するアカウント」が部屋を登録したオーナーとなります。よって、部屋のオーナーにトークンが転送されます。

        // 宿泊料を部屋のオーナーへ支払う
Promise::new(owner_id).transfer(deposit);

次に、オーナーがチェックイン・チェックアウトの操作をするためのメソッドを実装します。各操作の定義は、以下とします。

  • チェックイン : 部屋の利用状況をStayにする。
  • チェックアウト : 部屋の利用状況をAvailableに戻し、対象の予約データを削除する。

それでは、以下のコードを追加しましょう。

/contract/src/lib.rs

    pub fn exists(&self, owner_id: AccountId, room_name: String) -> bool {
...
}

// 部屋を予約する
#[payable]
pub fn book_room(&mut self, room_id: RoomId, check_in_date: CheckInDate) {
let room = self
.rooms_by_id
.get_mut(&room_id)
.expect("ERR_NOT_FOUND_ROOM");

let account_id = env::signer_account_id();

// 関数コール時に送付されたNEARを取得
let deposit = env::attached_deposit();
// 送付されたNEARと実際の宿泊料(NEAR)を比較するためにキャストをする
let room_price: u128 = room.price.into();
assert_eq!(deposit, room_price, "ERR_DEPOSIT_IS_INCORRECT");

// 予約が入った日付, 宿泊者IDを登録
room.booked_info
.insert(check_in_date.clone(), account_id.clone());

// 宿泊者に予約データを保存
let owner_id = room.owner_id.clone();
self.add_booking_to_guest(account_id, room_id, check_in_date);

// 宿泊料を部屋のオーナーへ支払う
Promise::new(owner_id).transfer(deposit);
}

+ // 部屋の利用状況を`Stay -> Available`に変更する
+ pub fn change_status_to_available(
+ &mut self,
+ room_id: RoomId,
+ check_in_date: CheckInDate,
+ guest_id: AccountId,
+ ) {
+ let mut room = self
+ .rooms_by_id
+ .get_mut(&room_id)
+ .expect("ERR_NOT_FOUND_ROOM");
+
+ // 部屋が持つ予約データの削除
+ room.booked_info
+ .remove(&check_in_date)
+ .expect("ERR_NOT_FOUND_DATE");
+
+ // 利用状況を`Available`に変更
+ room.status = UsageStatus::Available;
+
+ // 宿泊者が持つ予約データを削除
+ self.remove_booking_from_guest(guest_id, check_in_date);
+ }
+
+ // 部屋の利用状況を`Available -> Stay` に変更する
+ pub fn change_status_to_stay(&mut self, room_id: RoomId, check_in_date: CheckInDate) {
+ let mut room = self
+ .rooms_by_id
+ .get_mut(&room_id)
+ .expect("ERR_NOT_FOUND_ROOM");
+
+ // 利用状況を`Stay`に変更
+ room.status = UsageStatus::Stay { check_in_date };
+ }
+
+ // changeメソッドの`change_status_to_stay`を実行する前に、部屋の利用状況を確認する
+ pub fn is_available(&self, room_id: RoomId) -> bool {
+ let room = self.rooms_by_id.get(&room_id).expect("ERR_NOT_FOUND_ROOM");
+
+ if room.status != UsageStatus::Available {
+ // 利用状況が既に`Stay`の時
+ return false;
+ }
+ // 利用状況が`Available`の時
+ true
+ }

pub fn get_rooms_registered_by_owner(&self, owner_id: AccountId) -> Vec<ResigteredRoom> {
...
}

追加した内容を見ていきましょう。

オーナーが部屋の利用状況を変更できる機能を追加しました。これらは、Webアプリケーション上でオーナーがボタンを操作することにより実現されます。

change_status_to_availableメソッドは、オーナーがCheck Outボタンを押した際に、部屋の利用状況をStayAvailableに変更します。 利用状況を変更したい部屋のIDが引数として渡されるので、データからIDに紐づく部屋を取得します。次に、取得した部屋のstatusを変更します。これは、宿泊者がチェックアウトをしたことを意味するので、予約データからremove()というメソッドを使用してcheck_in_dateを削除します。

        // 部屋が持つ予約データの削除
room.booked_info
.remove(&check_in_date)
.expect("ERR_NOT_FOUND_DATE");

// 利用状況を`Available`に変更
room.status = UsageStatus::Available;

最後に、宿泊者の予約データを削除します。この操作は、Contract構造体が持つbookings_per_guestデータに対して行われます。remove_booking_from_guestメソッドは後ほど追加します。

        // 宿泊者が持つ予約データを削除
self.remove_booking_from_guest(guest_id, check_in_date);

change_status_to_stayメソッドは、オーナーがCheck Inボタンを押した際に、部屋の利用状況をAvailableStayに変更します。IDから部屋を取得する部分は、change_status_to_availableメソッドと同じです。違いは、以下の部分です。

         // 利用状況を`Stay`に変更
room.status = UsageStatus::Stay { check_in_date };

statusを変更する際に、どの宿泊データ(宿泊日)が使用されているのかを周知するために、check_in_dateを保存します。

is_availableメソッドは、オーナーが誤ってCheck Inボタンを押してしまわないように確認をするためのものです。フロントエンドを実装する際に、change_status_to_stayメソッドを呼び出す前にこのメソッドを使用します。 確認したい部屋のIDが引数として渡されるので、IDに紐づく部屋を取得してstatusを確認します。

        if room.status != UsageStatus::Available {
// 利用状況が既に`Stay`の時
return false;
}
// 利用状況が`Available`の時
true

戻り値はbool型で、Stayならfalseを、Availableならtrueを返します。

では最後に、不足していたメソッドを実装しましょう。ここで追加するメソッドは、

#[near_bindgen]
impl Contract {
...
}

このスコープの外に定義します。 定義する場所に注意して、以下のコードを追加しましょう。 /contract/src/lib.rs

#[near_bindgen]
impl Contract {
...
}

+ // Private functions
+ impl Contract {
+ // 予約データを宿泊者用に保存する
+ fn add_booking_to_guest(
+ &mut self,
+ guest_id: AccountId,
+ room_id: RoomId,
+ check_in_date: CheckInDate,
+ ) {
+ match self.bookings_per_guest.get_mut(&guest_id) {
+ // 宿泊者が既に別の予約データを所有している時
+ Some(booked_date) => {
+ booked_date.insert(check_in_date, room_id);
+ }
+ // 初めて予約データを保存する時
+ None => {
+ let mut new_guest_date = HashMap::new();
+ new_guest_date.insert(check_in_date, room_id);
+ self.bookings_per_guest.insert(guest_id, new_guest_date);
+ }
+ }
+ }
+
+ // 宿泊者の持つ予約データから、宿泊済みのデータを削除する
+ fn remove_booking_from_guest(&mut self, guest_id: AccountId, check_in_date: CheckInDate) {
+ // 宿泊者が持っている予約データのmapを取得
+ let book_info = self
+ .bookings_per_guest
+ .get_mut(&guest_id)
+ .expect("ERR_NOT_FOUND_GUEST");
+
+ book_info
+ .remove(&check_in_date)
+ .expect("ERR_NOT_FOUND_BOOKED");
+
+ // 予約データが空になった場合、`bookings_per_guest`からゲストを削除する
+ if book_info.is_empty() {
+ self.bookings_per_guest.remove(&guest_id);
+ }
+ }
+ }

追加した内容を見ていきましょう。

add_booking_to_guestメソッドは、宿泊者が予約をした際に宿泊データを登録するためのものです。これは、book_roomメソッド内で呼ばれます。中で行っていることは、add_room_to_ownerと一緒で、match文を使用して既に宿泊データが保存されているか、初めて保存されるのかに応じて処理を分けています。

remove_booking_from_guestメソッドは、引数で削除したい宿泊データの日付check_in_dateが渡されます。このデータをContract構造体が所有するbookings_per_guestから削除します。これは、change_status_to_availableメソッド内で呼ばれます。

2つのメソッドには、fnキーワードの前にpubキーワードがありません。これは、lib.rs内部でのみ呼ばれる(プライベート)メソッドであるためです。

    fn add_booking_to_guest(
    fn remove_booking_from_guest(&mut self, guest_id: AccountId, check_in_date: CheckInDate) {

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#nearで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の4点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

長かったですが、これでスマートコントラクトの予約機能が完成しました!

次のレッスンに進み、予約機能をテストしてみましょう!